LINUX设备驱动程序笔记(三)字符设备驱动程序

      <一>.主设备号和次设备号
       对字符设备的訪问时通过文件系统内的设备名称进行的。那些设备名称简单称之为文件系统树的节点,它们通常位于/dev文件夹。

字符设备驱动程序的设备文件可通过ls -l命令输出的第一列中的'c'来识别。

块设备相同位于/dev下,由字符'b'标识
crw-rw----  1 root root    253,   0 2013-09-11 20:33 usbmon0
crw-rw----  1 root root    253,   1 2013-09-11 20:33 usbmon1
crw-rw----  1 root root    253,   2 2013-09-11 20:33 usbmon2
brw-rw----  1 root disk      8,   0 2013-09-11 20:33 sda
brw-rw----  1 root disk      8,   1 2013-09-11 20:34 sda1
brw-rw----  1 root disk      8,   2 2013-09-11 20:33 sda2
       主设备号标识设备相应的驱动程序,现代的Linux内核同意多个驱动程序共享主设备号,但大多数设备仍然依照"一个主设备相应一个驱动程序"的原则组织。


       次设备号由内核使用,用于正确确定设备文件所指的设备。能够通过次设备号获得一个指向内核设备的直接指针,也可将次设备号当做设备本地数组的索引。无论用哪种方式。处理知道次设备号用来指向驱动程序所实现的设备之外,内核本身基本不关心关于次设备号的不论什么其它信息。
       1.设备标号的内部表达
       在内核中,dev_t类型(在<linux/types.h>中定义)用来保存设备编号----包含主设备号和次设备号。要获得dev_t的主设备号和次设备号。要使用<linux/kdev_t.h>中定义的宏MAJOR/MINOR:MAJOR(dev_t dev); MINOR(dev_t dev);相反,假设须要将主设备号与次设备号转换为dev_t类型。则使用MKDEV(int major, int minor);
       2.分配和释放设备编号
      在建立一个字符设备前,首先要做的就是获得一个或者多个设备编号。

完毕该工作的必要函数在<linux/fs.h>中定义:
int register_chrdev_region(dev_t first, unsigned int count, char *name);
       a.first是要分配的设备编号范围的起始值。first的此设备号常常被置为0。但对该函数来讲并非必须的。
       b.count是所请求的连续设备编号的个数。
       c.name是和该编号范围关联的设备名称,它将出如今/proc/devices和sysfs中。
       d.register_chrdev_region的返回值在分配成功时为0,在错误情况下,将返回一个负的错误码。而且不能使用所请求的编号区域。


       

      假设不知道设备将要使用哪些主设备号。就要使用alloc_chrdev_region动态分配设备编号,

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
      a.dev是仅用于输出的參数。在成功调用后将保存已分配范围的第一个编号。
      b.firstminor应该是要使用的被请求的第一个次设备号,它一般是0。 
      c.count和name參数与register_chrdev_region函数式一样的。
     不论採用哪种方法分配设备编号。都应该在不再使用它们时释放这些设备编号,设备编号的释放须要使用以下的函数void unregister_chrdev_region(dev_t first, unsigned int count);通常我们在清除函数中调用nregister_chrdev_region函数。
       在用户空间程序能够訪问上述设备编号之前。驱动程序须要将设备编号和内部函数连接起来。这些内部函数用来实现设备的操作。
       3.动态分配主设备号一部分主设备号已经静态地分配给了大部分常见设备。

在内核源代码树的Documentation/devicex.txt文件里能够找到这些设备的清单。对于一个驱动程序。讲义不要随便选择一个当前未使用的设备号作为主设备号,而应该使用动态分配机制获取主设备号。
        动态分配的缺点是:因为分配的主设备号不能保证始终一致,所以无法预先创建设备节点。为了载入一个使用动态主设备号的设备驱动程序,对insmod的调用可替换为一个简单的脚本,该脚本在调用insmod之后读取/proc/devices以获得新分配的主设备号,然后创建相应的设备文件。分配主设备号的最佳方式是:默认採用动态分配。同一时候保留在载入甚至是编译时指定主设备号的余地。


       <二> 一些重要的数据结构
        大部分主要的驱动程序操作涉及到三个重要的内核数据结构,各自是file_operations、file和inode。


        1.文件操作
         file_operations结构用来建立连接设备编号和驱动程序操作。该结构定义在<linux/fs.h>中。当中包括了一组函数指针。每一个打开的文件(后面提到的file)和一组函数关联。驱动程序的操作主要用来实现系统调用,命名为open、read等,能够觉得文件时一个“对象”。而操作它的函数式“方法”。file_operations结构或者指向这类结构的指针称为fops。这个结构中的每一个字段都必须指向驱动程序中实现特定操作的函数。对于不支持的操作,相应的字段可置为NULL值。对于各个函数而言,假设相应字段被赋为NULL指针,那么内核的详细处理行为不尽同样。
       在file_operations里。有很多參数包括有__user字符串,它事实上是一种形式的文档而已,表明指针式一个用户空间地址。因此,不能被直接引用。
        struct module *owner:指向“拥有”该结构的模块的指针。内核使用这个字段以避免在模块的操作正在被使用时卸载该模块。该成员被初始化为THIS_MODULE,它是定义在<linux/module.h>中
        int (*open)(struct inode *, struct file *):这是对设备文件的第一个操作,然而却不要求驱动程序一定要声明一个相
应的方法。

假设这个入口为NULL,设备的打开操作永远成功,但系统不会通知驱动程序。


        int (*release)(struct inode *, struct file *):当file结构被释放时。将调用这个操作。与open相仿,也可将release设
置为NULL,release并非在进程每次调用close时都会被调用。仅仅要file结构被共享,release就会等到全部的副本都关闭之后才会得到调用。

假设须要关闭随意一个副本时刷新那些待处理的数据。则应事先flush方法。
        int (*flush)(struct file *):对flush操作的调用发生在进程关闭设备文件描写叙述符副本的时候,它应该运行设备上尚未完结的操作。

假设flush被置为NULL,内核将简单忽略用户程序程序的请求。


        unsigned int (*poll)(struct file *, struct poll_table_struct *):poll方法是poll/epoll和select这三个系统调用的后端实现。poll方法应该返回一个位掩码,用来指出非堵塞的读取或写入是否可能,而且也会向内核提供调用进程置于休眠状态直到I/O变为可能时的信息。假设驱动程序将poll方法定义为NULL。则设备会被觉得既可读也可写。而且不会被堵塞。
        ssize_t (*read)(struct file *, char __user *, size_t, lofft_t *):用来从设备中读取数据。该函数指针被赋予NULL时,将导致read系统调用出错并返回-EINVAL。函数返回非负值表示成功读取的字节数。


        ssize_t (*write)(struct file *, const char __user *, size_t, loff_t):向设备发送数据,假设没有这个函数。write
系统调用会向程序返回一个-EINVAL,假设返回值非负。则表示成功写入的字节数。


        2.file结构
        在<linux/fs.h>中定义的struct file是设备驱动程序所使用的第二个重要的数据结构。

注意,file结构与用户空间程序中的FILE没有不论什么关联。

FILE在C库中定义且不会出如今内核代码中。而struct file是一个内核结构。不会出如今用户程序中。
        file结构代表一个打开的文件。它由内核在open时创建。并传递给在该文件上进行操作的全部函数,知道最后的close函数。在文件的全部实例都被关闭之后。内核会释放这个数据结构。struct file中最重要的成员罗列例如以下:
mode_t f_mode:文件模式,它通过FMODE_READ和FMODE_WRITE位来标识文件是否可读或可写,因为内核在调用驱动程序的read和write前已经检查了訪问权限。所以不必为这两个方法检查权限。在没有获得相应訪问权限而打开文件的情况下,对文件的读写操作将被内核拒绝,驱动程序无需为此而作额外的推断。


        unsigned int f_flags:文件标志,如O_RDONLY/O_NONBLOCK/O_SYNC。为了检查用户请求是否是非堵塞式的操作。驱动程序须要检查O_NONBLOCK标志,而其它标志非常少用到。

注意,检查读/写权限应该查看f_mode而不是f_flags。全部这些标志都定义在<linux/fcntl.h>中loff_t f_pos:当前的读/写位置。loff_t是一个64位的数。假设驱动程序须要知道文件里的当前位置,能够读取这个值,但不要去改动它。read/write会使用它们接收到的最后那个指针參数来更新这个位置,而不是直接对file->f_pos进行操作。这个规则的一个例外是llseek方法,该方法的目的本身就是改动文件位置。
       struct file_operations *f_op:与文件相关的操作。内核在运行open操作时对这个指针赋值。以后须要处理这些操作时读取这个指针。file->f_op中的值决不会为方便引用而保存起来,也就是说。我们能够在不论什么须要的时候改动文件的关联操作,在返回给调用者之后,新的操作方法就会马上生效。
       void *private_data:open系统调用在调用驱动程序的open方法前将这个指针置为NULL。驱动程序能够将这个字段用于不论什么目的或者忽略这个字段。
       3.inode结构
      内核用inode结构在内部表示文件。因此它和file结构不同。后者表示打开的文件描写叙述符。对单个文件。可能会有很多个表示打开的文件描写叙述符的file结构,但他们都指向单个inode结构。该结构中仅仅有两个字段对编写驱动程序实用:
       dev_t i_rdev:对表示设备文件的inode结构。该字段包括了真正的设备编号
       struct cdev *i_cdev:struct cdev是表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,该字段包好了指向struct cdev结构的指针。

为了防止内核版本号的升级带来的不兼容问题,一般不直接使用i_rdev。而是使用以下的宏获得设备号:
        unsigned int iminor(struct inode *inode);  unsigned int imajor(struct inode *inode);


       <三>字符设备的注冊
       在内核调用设备的操作之前,必须分配并注冊一个或多个struct cdev结构。

为此。必须包括<linux/cdev.h>,当中定义了该结构及相关的辅助函数:
       分配、初始化struct cdev结构的两种方式:
struct cdev *my_cdev = cdev_allo();
void cdev_init(struct cdev *cdev, struct file_operations *fops);
my_cdev->ops = &my_fops;
        另一个struct cdev的字段须要初始化,和file_operations结构类似,struct cdev也有一个全部者字段。应被设为THIS_MODULE。


        在cdev结构设置好之后。最后的步骤就是通过以下的调用告诉内核该结构的信息:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);

        dev是cdev结构,num是该设备相应的第一个设备编号,count是应该和该设备关联的设备编号的数量,count常常取1。在使用cdev_add时,须要注意:

a.这个调用可能会失败。

假设它返回一个负的错误满,则设备不会被加入到系统中。

b.仅仅要cdev_add返回了,设备的操作就会被内核调用。因此。在驱动程序还没有全然准备优点理设备上的操作时。就不能调用cdev_add。

         要从系统中移除一个字符设备。做例如以下调用:void cdev_del(struct cdev *dev);将dev传递给cdev_del函数之后。就不应再訪问cdev结构了。


早起的办法:

       注冊字符设备驱动程序的经典方式:int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);假设使用register_chrdev函数,将自己的设备从系统移除的正确函数是:int unregister_chrdev(unsigned int major, const char *name);


      <四>open和release
      1.open方法:open方法提供给驱动程序以初始化的能力,从而为以后的操作完毕初始化做准备。在大部分驱动程序中。open应完毕例如以下工作:

     a.检查设备特定的错误 

     b.假设设备是首次打开。则对其进行初始化 

     c.假设有必要。更新f_op指针 

     d.分配并填写置于filp->private_data里的数据结构

     首先要做的是确定要打开的详细设备,open方法原型是:int (*open)(struct inode *inode,struct file *filp);当中的inode參数在其i_cdev字段中包括了我们所须要的信息,即我们先前设置的cdev结构。

唯一的问题是,我们通常不须要cdev结构本身,而是希望得到包括cdev结构的scull_dev结构。

通过定义在<linux/kernel.h>中的container_of宏实现:container_of(pointer, container_type, container_field);这个宏须要一个container_field字段的指针。该字段包括在container_type类型的结构,然后返回包括该字段的结构指针。
struct scull_dev *dev; /*device information*/
dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /*for other methods*/
      还有一个确定要打开的设备的方法是:检查保存在inode结构体中的次设备号。
       2.release方法:release方法的作用正好与open相反,这个设备方法都应该完毕以下的任务:

       a.释放由open分配的、保存在filp->private_data中的全部内容。

       b.在最后一側关闭操作时关闭设备


      <五>read 和 write
read和write方法完毕的任务相似,即拷贝数据到应用程序空间,或反过来从应用程序空间拷贝数据。
ssize_t read(struct file *filp, char __user *buff, size_t conut, loff_t *offp);
ssize_t write(struct file *filp, char __user *buff, size_t conut, loff_t *offp);
       參数filp是文件指针。參数count是请求传输的数据长度。參数buff是指向用户空间的缓冲区。这个缓冲区或者保护要写入的数据,或者是一个存放读入数据的空缓冲区。最后的offp是一个指向"long offset type"对象的指针。这个对象指明用户在文件里进行存取操作的位置。
      须要指出,read和write方法的buff參数是用户空间的指针,因此,内核代码不能直接引用当中的内容。出现这样的限制的原因有例如以下几个:
      a.随着驱动程序所执行的架构的不同或者内核配置的不同,在内核模式中执行时,用户空间的指针可能是无效的。

该地址可能根本无法被映射到内核空间,或者可能指向某些随机数据。
      b.即使该指针在内核空间中代表同样的东西,但用户空间的内存是分页的,而在系统调用被调用时,涉及到的内存可能根本不在RAM中。对用户空间内存的直接引用将导致页错误,而这对内核代码来说是不同意的发生的,其结果可能是一个"oops",它将导致调用该系统调用的进程的死亡。
      c.我们讨论的指针可能由用户程序提供。而该程序可能存在缺陷或者是个恶意程序。假设驱动程序盲目引用用户提供的指针,将导致系统出现打开的后门。从而同意用户空间程序訪问或覆盖系统中的内存。假设读者不打算由于自己的驱动程序而危及用户系统的安全性,则永远不要直接引用用户空间指针。
       read和write代码要做的就是在用户地址空间和内核地址空间之间进行整段数据的拷贝。

read和write方法的实现核心在:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
unsigned long copy_from_user(void __user *to, const void *from, unsigned long count);

       这两个函数的作用并不限于在内核空间和用户空间之间拷贝数据。它们还检查用户空间的指针是否有效。

假设指针无效。就不会进行数据拷贝;还有一方面。假设在拷贝过程中遇到无效地址,则只会复制部分数据。在这两种情况下,返回值还须要拷贝的内存数量值。至于实际的设备方法,read方法的任务是从设备拷贝数据到用户空间。而write方法则是从用户空间拷贝数据到设备上。每次read或write系统调用都会请求一定数目的字节传输,只是驱动程序也并不限制小数据量的传输。

不管传输多少数据。都应更新*offp所表示的文件位置,以便反应在新系统调用成功完毕之后当前的文件位置。出错时,read和write方法都返回一个负值,大于等于0的返回值告诉调用程序成功传输了多少字节。

假设在正确传输部分数据之后发生了错误,则返回值必须是成功传输的字节数。虽然内核函数通过返回负值来表示错误,并且返回值表明了错误的类型,但执行在用户空间的程序看到的时钟是作为返回值的-1。


      1.read方法:
      调用程序对read的返回值解释例如以下:
      a.假设返回值等于传递给read系统调用的count參数。则说明所请求的字节数传输成功完毕了。
      b.假设返回值是正的。可是比count小,则说明仅仅有部分数据成功传送。

这样的情况因设备的不同可能有很多原因。大部分情况下。程序会又一次读数据。
      c.假设返回值为0,则表示已经到达了文件尾。
      d.负值意味着发生了错误,该值指明了发生了什么错误。错误码在<linux/errno.h>中定义。


      2.write方法:

     与read类似,依据例如以下返回值规则,write也能传输少于请求的数据量:
     a.假设返回值等于count,则完毕了所请求数目的字节传送
     b.假设返回值是正的,但小于count,则仅仅传输了部分数据。程序可能再次试图写入余下的数据。


     c.假设值为0,意味着什么也没写入,这个结果不是错误。并且也没有理由返回一个错误码。

     d.赋值意味着发生了错误,与read同样。有效的错误码定义在<linux/errno.h>中。

原文地址:https://www.cnblogs.com/llguanli/p/7103261.html