linux驅動開發學習一:建立一個字符設備

首先是內核初始化函數。代碼以下。主要是三個步驟。1 生成設備號。 2 註冊設備號。3 建立設備。node

#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)     生成設備號linux

咱們要註冊一個設備,首先要生成這個設備的設備號。這裏先分配一塊大小爲4KB的內存空間。同時將該值賦值給globalmem_major用於生成設備號shell

 Linux的設備管理是和文件系統緊密結合的,各類設備都以文件的形式存放在/dev目錄下,稱爲設備文件。應用程序能夠打開、關閉和讀寫這些設備文件,完成對設備的操做,就像操做普通的數據文件同樣。爲了管理這些設備,系統爲設備編了號,每一個設備號又分爲主設備號和次設備號。主設備號用來區分不一樣種類的設備,而次設備號用來區分同一類型的多個設備數組

以下在dev下的設備,中,都是以b開頭的。證實都是block設備。而後主設備號都是7,0,1,10都是次設備號dom

nb-test:/dev$ ls -al函數

brw-rw----   1 root disk      7,     0 10月 24 16:36 loop0oop

brw-rw----   1 root disk      7,     1 10月 24 16:36 loop1指針

brw-rw----   1 root disk      7,    10 10月 24 16:36 loop10對象

 

和設備號相關的代碼以下,blog

#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

相關文章
相關標籤/搜索