【Linux 驱动】第三章 字符设备驱动程序 (详细,优秀)

一,字符设备驱动(Character devices)

在I/O传输过程中以字符为单位进行传输的设备,例如键盘,打印机等

二,scull(Simple Character Utility for Loading Localities)区域装载的简单字符工具

是一个操作内存区域的字符设备驱动程序,这片内存区域就相当于一个字符设备。scull的优点在于他不和任何硬件相关,而只是操作从内核分配的一些内存。任何人都可以编译和运行scull,而且还看看可以将scull移植到linux支持的所有计算机平台上。但另一方面,除了展示内核和字符设备驱动程序之间的接口并且让用户运行某些测试例程外,scull设备做不了任何“有用”的事情。

三,主设备号和次设备号

1)字符设备驱动程序的设备文件,首先切换目录到/dev 下,然后通过ls -l 输出第一列如果是字符 ‘c’ 表明为字符设备驱动的设备文件

2)主设备号用来区分不同种类的设备(标识设备对应的驱动程序),而次设备号用来区分同一类型的多个设备(内核使用)。对于常用设备,Linux有约定俗成的编号,如硬盘的主设备号是3。

3)设备号是在驱动module 中分配并注册的,/dev 下的设备文件是根据这个设备号建立的,所以当访问 /dev下的设备文件时,驱动module就知道,此时内核就应该让驱动提供服务了。

4)在内核中,用dev_t类型(其实就是一个32位的无符号整数)的变量来保存设备的主次设备号,其中高12位表示主设备号,低20位表示次设备号。 设备获得主次设备号有两种方式:一种是手动给定一个32位数,并将它与设备联系起来(即用某个函数注册);另一种是调用系统函数给设备动态分配一个主次设备号。 5)对于手动给定一个主次设备号,使用以下函数: int register_chrdev_region(dev_t first, //手动给定的设备号 unsigned int count, //请求的连续设备号的个数 char *name) //name是和该设备号范围关联的设备名称,它将出现在/proc/devices和sysfs中。

【补充】proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。用户和应用程序可以通过proc得到系统的信息,并可以改变内核的某些参数。由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取proc文件时,proc文件系统是动态从系统内核读出所需信息并提交的。

【说明】分配成功返回0,失败返回负数 比如,若first为0x3FFFF0,count为0x5,那么该函数就会为5个设备注册设备号,分别是0x3FFFF0、 0x3FFFF1、 0x3FFFF2、 0x3FFFF3、 0x3FFFF4,其中0x3(高12位)为这5个设备所共有的主设备号(也就是说这5个设备都使用同一个驱动程序)。而0xFFFF0、 0xFFFF1、 0xFFFF2、 0xFFFF3、 0xFFFF4就分别是这5个设备的次设备号了。

【注意】若count的值太大了,那么所请求的设备号范围可能会和下一个主设备号重叠。比如若 first还是为0x3FFFF0,而count为0x11,那么first+count=0x400001,也就是说为最后两个设备分配的主设备号已经不是0x3,而是0x4了!

【缺点】那就是若该驱动module被其他人广泛使用,那么无法保证注册的设备号是其他人的 Linux系统中未分配使用的设备号。 6)对于动态分配设备号,使用以下函数: int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name) 该函数需要传递给它指定的第一个次设备号firstminor(一般为0)和要分配的设备数count,以及设备名,调用该函数后自动分配得到的设备号保存在dev中。

【缺点】那就是无法预先在/dev下创建设备节点,因为动态分配设备号不能保证在每次加载驱动module时始终一致(其实若在两次加载同一个驱动module之间并没有加载其他的module,那么自动分配的设备号还是一致的,因为内核分配设备号并不是随机的,但是书上说某些内核开发人员预示不久的将来会用随机方式进行处理),不过,这个缺点可以避免,因为在加载驱动module后,我们可以读取/proc/devices文件以获得Linux内核分配给该设备的主设备号。 鉴于以上优缺点,强烈建议不要不要随便是哟功能一个当前未使用的设备号作为主设备号,所以使用动态分配机制获取主设备号。 7)释放设备号

无论采用哪种方法分配设备号,都采用下面函数释放

void unregister_chrdev_region(dev_t first,unsigned int count) 8)与主次设备号相关的3个宏: MAJOR(dev_t dev):根据设备号dev获得主设备号; MINOR(dev_t dev):根据设备号dev获得次设备号;

MKDEV(int major, int minor):根据主设备号major和次设备号minor构建设备号。

四,一些重要的数据结构

1)<linux/fs.h>中

struct file_operations {struct module *owner; //指向拥有这个结构的模块的指针.loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);int (*readdir) (struct file *, void *, filldir_t);unsigned int (*poll) (struct file *, struct poll_table_struct *);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long (*compat_ioctl) (struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, int datasync);int (*aio_fsync) (struct kiocb *, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **);long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);};

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 系统调用的程序. 如果非负, 返回值代表成功写的字节数. (注:这个操作和上面的对文件进行读的操作均为阻塞操作) ssize_t (*aio_write)(struct kiocb *, const char __user * buffer, size_t count, loff_t * ppos); 初始化设备上的一个异步写.参数类型同aio_read()函数; int (*readdir) (struct file * filp, void *, filldir_t); 对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用. 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()函数。 int (*flush) (struct file *); flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用; SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求. int (*release) (struct inode *, struct file *); release ()函数当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数: void release(struct inode inode,struct file *file),release函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。 在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL. int(*synch)(struct file *,struct dentry *,int datasync); 刷新待处理的数据,允许进程把所有的脏缓冲区刷新到磁盘。 int (*aio_fsync)(struct kiocb *, int); 这是 fsync 方法的异步版本.所谓的fsync方法是一个系统调用函数。系统调用fsync 把文件所指定的文件的所有脏缓冲区写到磁盘中(如果需要,还包括存有索引节点的缓冲区)。 相应的服务例程获得文件对象的地址,并随后调用fsync方法。通常这个方法以调用函数__writeback_single_inode()结束, 这个函数把与被选中的索引节点相关的脏页和索引节点本身都写回磁盘。 int (*fasync) (int, struct file *, int); 这个函数是系统支持异步通知的设备驱动,下面是这个函数的模板: static int ***_fasync(int fd,struct file *filp,int mode) { struct ***_dev * dev=filp->private_data; return fasync_helper(fd,filp,mode,&dev->async_queue);//第四个参数为 fasync_struct结构体指针的指针。 //这个函数是用来处理FASYNC标志的函数。(FASYNC:表示兼容BSD的fcntl同步操作)当这个标志改变时,驱动程序中的fasync()函数将得到执行。

} 此操作用来通知设备它的 FASYNC 标志的改变. 异步通知是一个高级的主题, 在第 6 章中描述. 这个成员可以是NULL 如果驱动不支持异步通知. int (*lock) (struct file *, int, struct file_lock *); lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它. ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); 这些方法实现发散/汇聚读和写操作. 应用程序偶尔需要做一个包含多个内存区的单个读或写操作; 这些系统调用允许它们这样做而不必对数据进行额外拷贝. 如果这些函数指针为 NULL, read 和 write 方法被调用( 可能多于一次 ). ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *); 这个方法实现 sendfile 系统调用的读, 使用最少的拷贝从一个文件描述符搬移数据到另一个. 例如, 它被一个需要发送文件内容到一个网络连接的 web 服务器使用. 设备驱动常常使 sendfile 为 NULL. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); sendpage 是 sendfile 的另一半; 它由内核调用来发送数据, 一次一页, 到对应的文件. 设备驱动实际上不实现 sendpage. unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); 这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中. 这个任务通常由内存管理代码进行; 这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动可以置这个方法为 NULL.[10] int (*check_flags)(int) 这个方法允许模块检查传递给 fnctl(F_SETFL…) 调用的标志. int (*dir_notify)(struct file *, unsigned long); 这个方法在应用程序使用 fcntl 来请求目录改变通知时调用. 只对文件系统有用; 驱动不需要实现 dir_notify. 一般情况下,进行设备驱动程序的设计只是比较注重下面的几个方法: struct file_operations ***_ops={ .owner = THIS_MODULE, .llseek = ***_llseek, .read = ***_read, .write = ***_write, .ioctl = ***_ioctl, .open = ***_open, .release = ***_release, };

2)注册字符设备的方法

intregister_chrdev(unsigned int major, const char *name,struct file_operations *fops); major是为设备驱动程序向系统申请的主设备号,为0则系统为此驱动程序动态地分配一个主设备号。

name是设备名。

fops就是前面所说的对各个调用的入口点的说明。此函数返回0表示成功。返回-EINVAL表示申请的主设备号非法,一般来说是主设备号大于系统所允许的最大设备号。返回-EBUSY表示所申请的主设备号正在被其它设备驱动程序使用。如果是动态分配主设备号成功,此函数将返回所分配的主设备号。

如果register_chrdev操作成功,设备名就会出现在/proc/devices文件里。 在成功的向系统注册了设备驱动程序后(调用register_chrdev()成功后),就可以用mknod命令来把设备映射为一个特别文件,其它程序使用这个设备的时候,只要对此特别文件进行操作就行了。

3)将设备从系统移除

int unregister_chrdev(unsigned int major, const char *name)

4)open和release 函数

1>open()函数:对设备特殊文件进行open()系统调用时,将调用驱动程序的open() 函数:

   int (*open)(struct inode * ,struct file *);

   其中参数inode为设备特殊文件的inode (索引结点) 结构的指针,参数file是指向这一设备的文件结构的指针。open()的主要任务是确定硬件处在就绪状态、验证次设备号的合法性(次设备号可以用 MINOR(inode- i – rdev) 取得)、控制使用设备的进程数、根据执行情况返回状态码(0表示成功,负数表示存在错误)等;

   2>release()函数:当最后一个打开设备的用户进程执行close ()系统调用时,内核将调用驱动程序的release() 函数:

   void (*release) (struct inode * ,struct file *) ;

   release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。

5)其他各种函数

1> read()函数:当对设备特殊文件进行read() 系统调用时,将调用驱动程序read()函数:

   ssize_t (*read) (struct file *, char *, size_t, loff_t *);

   用来从设备中读取数据。当该函数指针被赋为NULL 值时,将导致read 系统调用出错并返回-EINVAL("Invalid argument,非法参数")。函数返回非负值表示成功读取的字节数(返回值为"signed size"数据类型,通常就是目标平台上的固有整数类型)。

   2> globalvar_read()函数中内核空间与用户空间的内存交互需要借助第2节所介绍的函数:

   static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off) {…copy_to_user(buf, &global_var, sizeof(int));…}

   3>write( ) 函数:当设备特殊文件进行write () 系统调用时,将调用驱动程序的write () 函数:

   ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

   向设备发送数据。如果没有这个函数,write 系统调用会向调用程序返回一个-EINVAL。如果返回值非负,则表示成功写入的字节数。

   4> globalvar_write函数中内核空间与用户空间的内存交互需要借助第2节所介绍的函数:

   static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t*off) {…copy_from_user(&global_var, buf, sizeof(int));…}

   5>ioctl() 函数:该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,函数原型为:

   int (*ioctl) (struct inode * ,struct file * ,unsigned int ,unsigned long);

   unsigned int参数为设备驱动程序要执行的命令的代码,由用户自定义,unsigned long参数为相应的命令提供参数,类型可以是整型、指针等。如果设备不提供ioctl 入口点,则对于任何内核未预先定义的请求, ioctl 系统调用将返回错误(-ENOTTY,"No such ioctl fordevice,该设备无此ioctl 命令")。如果该设备方法返回一个非负值,那么该值会被返回给调用程序以表示调用成功。

6> llseek()函数:该函数用来修改文件的当前读写位置,并将新位置作为(正的)返回值返回,原型为:

   loff_t (*llseek) (struct file *, loff_t, int);

   7> poll()函数:poll 方法是poll 和select 这两个系统调用的后端实现,用来查询设备是否可读或可写,或是否处于某种特殊状态,原型为:

   unsigned int (*poll) (struct file *, struct poll_table_struct *);

  

莫愁前路无知己,天下谁人不识君。

【Linux 驱动】第三章 字符设备驱动程序 (详细,优秀)

相关文章:

你感兴趣的文章:

标签云: