linux字符设备开发


本篇基于ldd3中第三章,原书自带的源码随着内核版本更新已经不能运行,代码需要进行升级,文章参考代码能在内核版本4.17.2运行。

1.   分配设备编号

建立一个字符驱动时,需要做的第一件事是获取一个或多个设备编号来使用.此目的必要的函数是 register_chrdev_region.

注册字符设备函数执行后会出现在/proc/devices和sysfs中:

int register_chrdev_region(dev_t from, unsigned count, const char *name)

  first是要分配的起始设备编号. first 的次编号部分常常是 0, 但是没有要求是那个效果. count 是请求的连续设备编号的总数. 注意, 如果 count 太大,要求的范围可能溢出到下一个次编号; 但是只要要求的编号范围可用, 一切都仍然会正确工作. name 是应当连接到这个编号范围 的设备的名子; 它会出现在 /proc/devices 和 sysfs 中.

  一些主设备编号是静态分派给最普通的设备的. 一个这些设备的列表在内核源码树的 Documentation/devices.txt 中.

  对于新驱动,建议使用动态分配来获取你的主设备编号, 而不是随机选取一个当前空闲的编号.使用 alloc_chrdev_region, 不是 register_chrdev_region.

这个alloc_chrdev_region是动态分配主设备号的,因为你可能不知道系统中哪些主设备号可以给你的驱动程序使用,动态分配的一个缺点就是不能提前分配设备节点(注:通过MKNOD来创建节点的):

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

            两个函数从名字上也可以区分,一个是注册,表示我有了主设备号只是想内核注册,而alloc_则是分配,意味着让内核来帮着allocate一下。

            注册对应的是注销,这个是驱动程序卸载时候必须做的,从哪里来还那里去,对应的函数是:

void unregister_chrdev_region(dev_t from, unsigned count)

另外,获取设备主设备号和次设备号,使用如下宏:

MAJOR(dev_t dev);

MINOR(dev_t dev);

将主、次设备号转换成一个设备号,如下:

MKDEV(int major, int minor);

2.   基础性的驱动操作

基础性的驱动操作包括 3 个重要的内核数 据结构, 称为 file_operations, file, 和 inode.

2.1     file_operation

  传统上, 一个 file_operation 结构或者其一个指针称为 fops( 或者它的一些变体). 结构中的每个成员 必须指向驱动中的函数, 这些函数实现一个特别的操作

            其中定义的操作函数并不是需要全部实现,根据具体驱动实现针对的函数功能即可。字符设备主要有一下函数需要实现:

owner,llseek,read,write,ioctl,open,release.

2.2     file

file结构表示一个打开的文件。在内核中指向file的指针经常叫做filp,就是file pointer,以免是file搞混。

2.3     inode

inode结构由内核在内部用来表示文件,代表打开文件描述符的文件结构是不同的。多个打开的描述符可能指向一个单个inode结构。

相对于字符设备驱动程序,我们先使用i_rdev和i_cdev。

dev_t i_rdev;//实际设备的节点

Struct cdev *i_cdev; //指向字符设备驱动程序指针

            现在可以通过宏如下,来获取节点的主、次设备号:

unsigned int iminor(struct inode *inode);

unsigned int imajor(struct inode *inode);

3.   字符设备注册

内核中使用cdev结构体来表示字符设备。可以通过cdev_alloc来分配。

然后使用cdev_init来初始化。

       我们可以将cdev结构体嵌入到我们自己的设备结构体中,这也正是例子所使用的方法。

            最后告诉内核添加进去,如果不告诉内核就是空有一身资源而无施展之处,通过函数cdev_add。一旦添加,那么内核就可能来骚扰设备,所以要确保所有都准备好的时候调用cdev_add函数。

            去除设备调用函数cdev_del.

4.   设备布局

设备由内存来模拟,其设备中的布局如下图。

linux字符设备开发

数据结构如下,scull_qset结构体非常简单,其实现一个链表的同时,每个元素同时指向块内存:

struct scull_qset {

        void **data;

        struct scull_qset *next;

};

设备的结构体如下:

struct scull_dev {

        struct scull_qset *data;  /* Pointer to first quantum set */

        int quantum;              /* the current quantum size */

        int qset;                 /* the current array size */

        unsigned long size;       /* amount of data stored here */

        unsigned int access_key;  /* used by sculluid and scullpriv */

        struct semaphore sem;     /* 互斥所*/

        struct cdev cdev;         /* Char device structure              */

};

设备的大小为quantum*qset。

 

5.   代码解析

5.1     初始化

初始化函数为scull_init_module

如果指定了主设备号,调用register_chrdev_region否则调用alloc_chrdev_region,并获取主设备号。

然后分配设备scull_dev结构体数组scull_devices,数量为SCULL_NR_DEVS子设备号数量,并初始化为0。接着根据需要分配的内存空间大小正式初始化scull_devices,其中会调用scull_setup_cdev函数(该函数中会使用cdev_init,cdev_add函数,初始化设备结构中嵌入的cdev,同时绑定scull_fops),scull_fops结构如下。

struct file_operations scull_fops = {

        .owner =    THIS_MODULE,

        .llseek =   scull_llseek,

        .read =     scull_read,

        .write =    scull_write,

        .unlocked_ioctl = scull_ioctl,

        .open =     scull_open,

        .release =  scull_release,

};

            然后调用

初始化时候分配了指定数量的设备数据结构,并初始化后增加到了内核,可以在/proc/devices中看到,此时其实并没有分配设备的内存空间,因为还不需要。

 

5.2     退出

退出函数是scull_cleanup_module,该函数先获取设备号。然后根据设备数量循环调用scull_trim,cdev_del函数来删除cdev设备,最后调用kfree释放在初始化中分配的结构体数据。

            其中scull_trim函数负责释放分配的内存空间。

5.3     操作函数集合

驱动的open函数

通过inode获取设备结构体的指针,这个通过内核中的container_of函数来实现,并将其保存到文件对象的private_data中以备后用。

如果是写模式打开,则将之前该设备分配的空间使用scull_trim函数清空。

 

llseek

返回当前读写位置。

 

release

直接返回0,并不做操作。

read

先从filp->private_data中获取设备地址。获取设备总空间大小。

如果要读的位置大于空间大小则退出。否则计算要读取的正确位置,因为每个scull_qset结构体指向的item空间大小是固定的,其相互之间是链表方式连接的。

然后会调用scull_follow函数,该函数中会通过kmalloc函数动态分配scull_qset结构体(如果没有被分配过),直到包含的item累计理论空间能包含要读取的地址。然后返回最后一个scull_qset结构体,如果返回null,说明系统内存空间不够了。

            最后调用copy_to_user函数,将内容复制到用户的buf中。然后更新文件读取位置,并返回所读取字节大小。

write

获取设备结构体的指针,以及对应的设备空间相关大小。如item,quantum。

计算文件读取位置,调用scull_follow,返回位置所在的那个item的scull_qset结构体,如果该结构体对应的数据指针为NULL,说明之前没有给其分配内存空间,则调用kmalloc分配qset指向quantum的指针数数组。

然后根据读取位置,通过函数kmalloc函数分配quantum的内存空间。

最后调用用copy_from_user函数将数据复制到内存中的quantum片段。

 

unlocked_ioctl

对ioctl的实现。

6.   使用测试

加载驱动后,执行如下,其中247是主设备号,在/proc/devices中可以查看到:

mknod /dev/scull0 c 247 0

mknod /dev/scull1 c 247 1

mknod /dev/scull2 c 247 2

mknod /dev/scull3 c 247 3

然后可以使用dd命令或者cp命令复制内容到设备中。

# echo "hello" > scull0

# cat scull0

hello

7.   代码

https://github.com/kernel-z/ldd3/tree/master/scull

 

 

 

 

 

上一篇:Windows server 2012 中power shell 3.0 基础知识-MVA微软官网实验环境


下一篇:bootstrap-表单控件——输入框input