欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

字符设备驱动(二)

程序员文章站 2024-02-01 15:09:34
...

  上一篇讲了应用层的程序,接下来我们继续往下走。
先来看看从应用层到驱动的调用过程。open会最终执行sys_open函数.
sys_open -> do_sys_open -> do_filp_open ->do_last-> nameidata_to_filp -> __dentry_open-> open = f->f_op->open(即调用chrdev_open)-> filp->f_op->open(inode,filp)(即为真正的file_operations中的open函数).
  简化一下就是上层open —-> 调用驱动层的file_operations中的open。每个应用层到了驱动都会有对应的驱动层的函数相对应。如果我们想写驱动那么完成对应的open、write等驱动函数就可以了。
  我们再总体简述一下字符设备、字符驱动和用户访问之间的关系。在Linux内核中使用cdev结构体来描述字符设备,通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性。通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等。在Linux字符设备驱动中,模块加载函数通过register_chrdev_region( ) 或alloc_chrdev_region( )来静态或者动态获取设备号,通过cdev_init( )建立cdev与file_operations之间的连接,通过cdev_add( )向系统添加一个cdev以完成注册。模块卸载函数通过cdev_del( )来注销cdev,通过unregister_chrdev_region( )来释放设备号。用户空间访问该设备的程序通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数。
  写字符驱动程序其实有点像是完型填空,即使你对linux不了解,按照一定的套路,也是可以写出一个字符设备驱动的。
  说了这么多不如来张图此图的地址来源http://www.cnblogs.com/chen-farsight/p/6177870.html
字符设备驱动(二)
  看了上图,结合文字,我觉的应该可以对字符设备有个大体的认识了吧。接下来我们就介绍一些字符设备需要用到的一些结构体,写驱动就是填充这些结构体,我们先把几个重要的结构体列出来,逐个分析。struct cdev和file_operations

/*  
*内核源码位置  
*linux2.6.38/include/linux/cdev.h  
*/  
struct cdev {  
    struct kobject kobj;  
    struct module *owner;   //一般初始化为:THIS_MODULE  
    const struct file_operations *ops;  //字符设备用到的例外一个重要的结构体     file_operations,cdev初始化时与之绑定  
    struct list_head list;  
    dev_t dev;  //主设备号24位 与次设备号8位,dev_t为32位整形  
    unsigned int count;  
}; 

  使用cdev去描述一个字符设备。cdev结构体中dev_t定义了设备号,为32位,12位位主设备,20位为次设备。我们可以使用以下宏,通过dev_t获取主次设备。
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //获得主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //获得此设备号
利用下面的宏通过主次设备号生成dev_t;
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //由主次设备号得到设备号

  另一个重要成员struct file_operations。这个结构体是程序设计的主题,里面包含的成员很多,我就不一一的进行讲解了,我们就先列出几个常用的。
来源:http://www.cnblogs.com/geneil/archive/2011/12/03/2272869.html

struct file_operations ***_ops={
 .owner =  THIS_MODULE,
 .llseek =  ***_llseek,
 .read =  ***_read,
 .write =  ***_write,
 .ioctl =  ***_ioctl,
 .open =  ***_open,
 .release = ***_release, 
....................
};
struct module *owner;
 /*第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针.
 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 
THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.这个宏比较复杂,在进行简单学习操作的时候,一般初始化为THIS_MODULE。*/
loff_t (*llseek) (struct file * filp , loff_t  p,  int  orig);
/*(指针参数filp为进行读取信息的目标文件结构体指针;参数 p 为文件定位的目标偏移量;参数orig为对文件定位
的起始地址,这个值可以为文件开头(SEEK_SET,0,当前位置(SEEK_CUR,1),文件末尾(SEEK_END,2))
llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值.
loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示.
如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).*/

ssize_t (*read) (struct file * filp, char __user * buffer, size_t    size , loff_t *  p);
/*(指针参数 filp 为进行读取信息的目标文件,指针参数buffer 为对应放置信息的缓冲区(即用户空间内存地址),
参数size为要读取的信息长度,参数 p 为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值)
这个函数用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败.
 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).*/

ssize_t (*aio_read)(struct kiocb *  , char __user *  buffer, size_t  size ,  loff_t   p);
/*可以看出,这个函数的第一、三个参数和本结构体中的read()函数的第一、三个参数是不同 的,
异步读写的第三个参数直接传递值,而同步读写的第三个参数传递的是指针,因为AIO从来不需要改变文件的位置。
异步读写的第一个参数为指向kiocb结构体的指针,而同步读写的第一参数为指向file结构体的指针,每一个I/O请求都对应一个kiocb结构体);
初始化一个异步读 -- 可能在函数返回前不结束的读操作.如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地).
(有关linux异步I/O,可以参考有关的资料,《linux设备驱动开发详解》中给出了详细的解答)*/

ssize_t (*write) (struct file *  filp, const char __user *   buffer, size_t  count, loff_t * ppos);
/*(参数filp为目标文件结构体指针,buffer为要写入文件的信息缓冲区,count为要写入信息的长度,
ppos为当前的偏移位置,这个值通常是用来判断写文件是否越界)
发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
(注:这个操作和上面的对文件进行读的操作均为阻塞操作)*/

unsigned int (*poll) (struct file *, struct poll_table_struct *);
/*(这是一个设备驱动中的轮询函数,第一个参数为file结构指针,第二个为轮询表指针)
这个函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。
每个宏都表明设备的一种状态,如:POLLIN(定义为0x0001)意味着设备可以无阻塞的读,POLLOUT(定义为0x0004)意味着设备可以无阻塞的写。
(poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞.
 poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 
如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.
(这里通常将设备看作一个文件进行相关的操作,而轮询操作的取值直接关系到设备的响应情况,可以是阻塞操作结果,同时也可以是非阻塞操作结果)*/

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
/*(inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数.
cmd 参数从用户那里不改变地传下来, 并且可选的参数 arg 参数以一个 unsigned long 的形式传递, 不管它是否由用户给定为一个整数或一个指针.
如果调用程序不传递第 3 个参数, 被驱动操作收到的 arg 值是无定义的.
因为类型检查在这个额外参数上被关闭, 编译器不能警告你如果一个无效的参数被传递给 ioctl, 并且任何关联的错误将难以查找.)
ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表.
 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.*/

int (*mmap) (struct file *, struct vm_area_struct *);
/*mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.
(如果想对这个函数有个彻底的了解,那么请看有关“进程地址空间”介绍的书籍)*/

int (*open) (struct inode * inode , struct file *  filp ) ;
/*(inode 为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构;
但是filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息)
 尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.
与open()函数对应的是release()函数。*/

  好了先消化一下,下篇开始介绍一些接口函数和字符驱动的模板,以后看懂了向里面填写就好了。