首先是內核初始化函數。代碼如下。主要是三個步驟。1 生成設備號。 2 注冊設備號。3 創建設備。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#define GLOBALMEM_SIZE 0X1000
#define MEM_CLEAR 0X1
#define GLOBALMEM_MAJOR 230
static int globalmem_major= GLOBALMEM_MAJOR;
module_param(globalmem_major,int,S_IRUGO);
struct globalmem_dev{
struct cdev cdev;
unsigned char mem[GLOBALMEM_SIZE];
};
static int __init globalmem_init(void)
{
int ret;
dev_t devno=MKDEV(globalmem_major,0); (1)
if(globalmem_major)
ret=register_chrdev_region(devno,1,"globalmem_tmp"); (2)
else{
ret=alloc_chrdev_region(&devno,0,1,"globalmem_tmp");
globalmem_major=MAJOR(devno);
}
if(ret < 0)
return ret;
globalmem_devp=kzalloc(sizeof(struct globalmem_dev),GFP_KERNEL);
if(!globalmem_devp){
ret=-EFAULT;
goto fail_malloc;
}
globalmem_setup_dev(globalmem_dev,0); (3)
return 0;
fail_malloc:
unregister_chrdev_region(devno,1);
return ret;
}
(1) 生成設備號
我們要注冊一個設備,首先要生成這個設備的設備號。這里先分配一塊大小為4KB的內存空間。同時將該值賦值給globalmem_major用於生成設備號
Linux的設備管理是和文件系統緊密結合的,各種設備都以文件的形式存放在/dev目錄下,稱為設備文件。應用程序可以打開、關閉和讀寫這些設備文件,完成對設備的操作,就像操作普通的數據文件一樣。為了管理這些設備,系統為設備編了號,每個設備號又分為主設備號和次設備號。主設備號用來區分不同種類的設備,而次設備號用來區分同一類型的多個設備
如下在dev下的設備,中,都是以b開頭的。證明都是block設備。然后主設備號都是7,0,1,10都是次設備號
nb-test:/dev$ ls -al
brw-rw---- 1 root disk 7, 0 10月 24 16:36 loop0
brw-rw---- 1 root disk 7, 1 10月 24 16:36 loop1
brw-rw---- 1 root disk 7, 10 10月 24 16:36 loop10
和設備號相關的代碼如下,
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
設備號是個32bit,高12bit是主設備號,低20bit是次設備號。MAJOR宏將設備號向右移動20位得到主設備號,MINOR將設備號的高12位清0。MKDEV將主設備號ma左移20位,然后與次設備號mi相與得到設備號。
(2) 注冊設備號
設備號生成,接下來的任務就是將設備號注冊到系統中去。由於我們是創建有一個字符型的設備,因此調用函數register_chrdev_region。
函數的原型:int register_chrdev_region(dev_t from, unsigned count, const char *name)
from是設備號,count是設備個數,name是設備名。實際上在里面調用的是
__register_chrdev_region 函數。這里面主要步驟包含幾個
>1 申請一個設備結構體內存
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
>2在chrdevs中找到cd的插入位置,在chrdevs中是以升序排列的。
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major ||
((*cp)->major == major &&
(((*cp)->baseminor >= baseminor) ||
((*cp)->baseminor + (*cp)->minorct > baseminor))))
break;
chrdevs是一個結構體指針數組,里面存儲的的都是每個結構體的指針。這里為什么要用到結構體指針數組,下面會介紹
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
>3 找到位置后,將cd插入到cp中去。這一段插入充分利用了指針的性質,在對於一個單鏈表的插入來說非常的巧妙。
cd->next = *cp;
*cp = cd;
cd和cp的類型申明如下。
struct char_device_struct *cd, **cp;
cd是char_device_struct的指針。cp是char_device_struct 指針的指針。在前面尋找插入位置的時候。循環控制方式如下,也就是說cp指向的是上一個節點的next指針的地址。
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
cd->next=*cp這個好理解,就是將cd的下一個節點指向*cp。那么*cp=cd相對比較抽象,這個的意思將cp地址存儲的內容修改為cd。而cp地址指向的是上一個節點的next指針地址,將整個*cp賦值為cd,也就是將上一個節點的next指針地址所存儲的值變為cd。這樣就實現了將cd插入到了鏈表中去
用段代碼來驗證下:
struct linklist
{
int num;
struct linklist *next;
};
int main(int argc, char **argv)
{
int i;
struct linklist head;
struct linklist_tmp *s;
head.num = 0;
head.next = NULL;
struct linklist *tmp = NULL;
struct linklist **ttmp = NULL;
len = sizeof(a)/sizeof(int);
for (i = 1; i < 6; i += 2)
{
tmp = (struct linklist *)malloc(sizeof(struct linklist));
tmp->num = i;
tmp->next = head.next;
head.next = tmp;
}
ttmp = &(head.next);
while (*ttmp)
{
printf("%d, %016x, %016x, %016x\n", (*ttmp)->num, ttmp, *ttmp, (*ttmp)->next);
ttmp = &((*ttmp)->next);
}
printf("============================\n");
struct linklist addnode = { .num = 2,.next = NULL };
ttmp = &(head.next);
while (*ttmp)
{
if ((*ttmp)->num < addnode.num)
{
break;
}
ttmp = &((*ttmp)->next);
}
addnode.next = *ttmp;
*ttmp = &addnode;
ttmp = &(head.next);
while (*ttmp)
{
printf("%d, %016x, %016x, %016x,%016x\n", (*ttmp)->num, ttmp, *ttmp, (*ttmp)->next,&((*ttmp)->next));
ttmp = &((*ttmp)->next);
}
return 0;
}
執行結果如下:
可以看到節點值為2 指針的指針就是以前節點值為1的地址。而節點值為1 指針的指針則被挪到了另外一個位置。

用下面這個圖來表示更直觀,*cp = cd; 也就意味着地址為1d7696c存儲的值變為0b3fab4,而地址0b3fab4存儲的節點就是插入的節點2。而0b3fab4指向節點1的地址也就是1d76930。而1d76930的地址則變為另外一個。

通過這種二級指針的方式實現了單鏈表的插入。這種方法避免了傳統的刪除或插入鏈表節點需要記錄鏈表prev節點。同樣的也可以用這種方式進行刪除節點
void remove_if(node ** head, remove_fn rm)
{
for (node** curr = head; *curr; )
{
node * entry = *curr;
if (rm(entry))
{
*curr = entry->next;
free(entry);
}
else
curr = &entry->next;
}
}
(3) Cdev的初始化和添加。
>1 首先是cdev的初始化。其中最重要的工作就是注冊設備的操作函數。設備的注冊函數實現如下。
static int globalmem_open(struct inode *inode,struct file *filp)
{
filp->private_data=globalmem_devp;
return 0;
}
static int globalmem_release(struct inode *inode,struct file *filp)
{
return 0;
}
static long globalmem_ioctl(struct file *filp,unsigned int cmd,unsigned long arg)
{
struct globalmem_dev *dev=filp->private_data;
switch(cmd)
{
case MEM_CLEAR:
memset(dev->mem,0,GLOBALMEM_SIZE);
printk(KERN_INFO "globalmem is set to zero\n");
default:
return -EINVAL;
}
return 0;
}
static ssize_t globalmem_read(struct file *filp,char __user *buf,size_t size,loff_t *ppos)
{
unsigned long p=*ppos;
unsigned int count=size;
int ret=0;
struct globalmem_dev *dev=filp->private_data;
if(p > GLOBALMEM_SIZE)
return 0;
if(count > GLOBALMEM_SIZE-p)
count=GLOBALMEM_SIZE-p;
if(copy_to_user(buf,dev->mem+p,count)){
ret=-EFAULT;
}
else{
*ppos+=count;
ret=count;
}
printk(KERN_INFO “read %u bytes(s) from %lu\n”,count,p);
return ret;
}
static ssize_t globalmem_write(struct file *filp,const char __user *buf,size_t size, loff_t *ppos)
{
unsigned long p=*ppos;
unsigned int count=size;
int ret=0;
struct globalmem_dev *dev=filp->private_data;
if(p > GLOBALMEM_SIZE)
return 0;
if(count > GLOBALMEM_SIZE-p)
count=GLOBALMEM_SIZE-p;
if(copy_from_user(dev->mem+p,buf,count))
ret=-EFAULT;
else{
*ppos+=count;
ret=count;
printk(KERN_INFO "written %u bytes(s) from %lu\n",count,p);
}
return ret;
}
static loff_t globalmem_llseek(struct file *filp,loff_t offset,int orig)
{
loff_t ret=0;
switch(orig){
case 0:
if (offset <0)
ret=-EFAULT;
break;
if ((unsigned int)offset > GLOBALMEM_SIZE){
ret=-EFAULT;
break;
}
filp->f_pos=(unsigned int)offset;
ret=filp->f_pos;
break;
case 1:
if((filp->f_pos+offset) > GLOBALMEM_SIZE){
ret=-EFAULT;
break;
}
if((filp->f_pos+offset) < 0){
ret=-EFAULT;
break;
}
filp->f_pos+=offset;
ret=filp->f_pos;
break;
}
return ret;
}
globalmem_fops就是操作的函數指針結構體。
static const struct file_operations globalmem_fops={
.owner=THIS_MODULE,
.llseek=globalmem_llseek,
.read=globalmem_read,
.write=globalmem_write,
.unlocked_ioctl=globalmem_ioctl,
.open=globalmem_open,
.release=globalmem_release,
};
cdev_init的工作就是將這些操作函數賦給cdev->ops
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
這里還有一個kobject_init函數,是用來初始化kobj對象的。這個下面介紹
>2 添加cdev設備。這里首先介紹kobj_map結構體
struct kobj_map {
struct probe {
struct probe *next; 鏈表結構
dev_t dev; 設備號
unsigned long range; 設備號的范圍
struct module *owner;
kobj_probe_t *get;
int (*lock)(dev_t, void *);
void *data; 指向struct cdev對象
} *probes[255];
struct mutex *lock;
};
結構體中有一個互斥鎖lock,一個probes[255]數組,數組元素為struct probe的指針。
根據下面的函數作用來看,kobj_map結構體是用來管理設備號及其對應的設備的。
kobj_map函數就是將指定的設備號加入到該數組,kobj_lookup則查找該結構體,然后返回對應設備號的kobject對象,利用利用該kobject對象,我們可以得到包含它的對象如cdev。struct probe結構體中的get函數指針就是用來獲得kobject對象的
因此cdev_add其實就是想kobj中添加設備的過程,具體實現是用kobj_map函數。
其中cdev_map是定義在char_dev.c中的一個靜態變量。
static struct kobj_map *cdev_map;
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
Kobj_map的代碼如下
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
struct module *module, kobj_probe_t *probe,
int (*lock)(dev_t, void *), void *data)
{
unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
unsigned index = MAJOR(dev);
unsigned i;
struct probe *p;
if (n > 255)
n = 255;
p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);
if (p == NULL)
return -ENOMEM;
for (i = 0; i < n; i++, p++) {
p->owner = module;
p->get = probe;
p->lock = lock;
p->dev = dev;
p->range = range;
p->data = data;
}
mutex_lock(domain->lock);
for (i = 0, p -= n; i < n; i++, p++, index++) {
struct probe **s = &domain->probes[index % 255];
while (*s && (*s)->range < range)
s = &(*s)->next;
p->next = *s;
*s = p;
}
mutex_unlock(domain->lock);
return 0;
}
至此設備的初始化,注冊,插入功能都已全部完成,下面來試下功能。Makefile文件如下
#Makefile文件注意:假如前面的.c文件起名為first.c,那么這里的Makefile文件中的.o文
#件就要起名為first.o 只有root用戶才能加載和卸載模塊
obj-m:=global_test.o #產生global_test模塊的目標文件
#目標文件 文件 要與模塊名字相同
CURRENT_PATH:=$(shell pwd) #模塊所在的當前路徑
LINUX_KERNEL:=$(shell uname -r) #linux內核代碼的當前版本
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
CONFIG_MODULE_SIG=n
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean #清理模塊
插入模塊:sudo insmod global_test.ko。 此時在/proc/devices下能看到多出了主設備號為230的globalmem_tmp字符設備驅動

接下來創建節點,執行命令sudo mknod -m 766 /dev/globalmem_tmp c 230 0。 顯示創建成功

cat /dev/globalmem_tmp 讀取設備數據。可以看到能正常的讀出數據
test:~/linux_prj/globalman$ cat /dev/globalmem_tmp
hello world
