LDD-Advanced Char Driver Operation

ioctl

ioctl是驱动程序向用户提供的控制设备的接口。用户空间的ioctl系统调用的形式如下:
int ioctl(int fd, unsigned long cmd, ...);
驱动程序的ioctl形式有所不同:
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

其中参数inode和filep与系统调用中的fd相对应,cmd和系统调用中的cmd一致,可选的参数为arg。

可以预见的是,ioctl的具体实现中包含大量的switch语句——根据cmd的值来执行不同的操作。
 
Linux的第一个版本采用16bit的cmd,高8位是和设备相关的魔数,低8位是连续的序列号,程序员需要根据include/asm/ioctl.h和Documentation/ioctl-number.txt来获得设备的cmd;这种方法已经被遗弃,但是仍有一些驱动程序采用这种构造方法。
现在cmd由四个bitfield组成,type、number、direction、size,见linux/ioctl.h。asm/ioctl.h中定义了创建cmd的宏,_IO,_IOR,_IOW,_IOWR,及相对的可以实现cmd解码的宏。
 
需要说明的是,内核能够识别一些cmd(预定义cmd)——这些cmd在传送到设备(调用设备自己的文件操作)之前会被解码。也就是说,如果用户自定的ioctl采用了这些cmd,不会有任何请求发出。
预定义cmd根据发送的对象分为三种:发送到任何文件(普通、设备、fifo、套接字)的、发送到普通文件的、发送到特定文件系统的。
下列预定义cmd适用于所有文件:FIOCLEX,FIONCLEX,FIOASYNC,FIOQSIZE,FIONBIO。
 
ioctl通常需要从用户空间接收数据,或者发送数据到用户空间,在传递大量数据时,可以采用copy_from_user和copy_to_user。
如果传送的数据较少(1,2,4,8byte),可以采用速度更快的方法put_user,get_user。
 
由于设备在Linux中以文件的形式管理,对于设备的访问权限控制就是对相应的设备文件的权限控制。内核提供了两个系统调用capget和capset供用户查询和设置文件的访问权限。访问权限的相关定义在linux/capability.h。
 
除了ioctl,可以直接向设备写入控制序列来控制设备,例如控制台驱动通过转义序列来移动光标等等。这种方式不用像ioctl一样,只能通过程序来配置设备,甚至可以从另外一个系统发送控制命令。
 
Blocking I/O

如果用户程序尝试对没有数据的设备进行读操作、或是对还未就绪接收数据的设备进行写操作,驱动程序需要将程序阻塞,直到能够处理程序的请求。
Linux设备驱动可以简单地使一个进程进入休眠状态,但是需要遵循以下规则:不能在互斥上下文中休眠,例如持有自选锁,seqlock,或者RCU lock;不能在关闭中断后休眠。
在程序从休眠状态中恢复时,需要检查所等待的条件确实已经满足——你不直到在休眠时发生了什么。
程序在休眠之前,一定要确定有别的程序会将其唤醒,而且一定要搞清楚什么事件能够将程序唤醒。
内核提供的进入休眠的方法(宏)有:
1 wait_event(queue, condition)
2 //不可中断的sleep
3 wait_event_interruptible(queue, condition)
4 //可中断的sleep
5 wait_event_timeout(queue, condition, timeout)
6 //休眠一段时间
7 wait_event_interruptible_timeout(queue, condition, timeout)
需要注意的是,宏中condition会在等待任何长度的事件后进行判断。
唤醒处于休眠状态的方法有:
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
 
linux/sched.h中定义进程的状态,通过
void set_current_state(int new_state);
可以设置当前进程的状态。但是并不会将当前的进程置于休眠状态,需要调用schedule()函数,交出CPU,才能真正进入休眠状态。
 
thundering herd:多个进程等待同一个条件,当这个条件满足时,只有一个进程能够继续执行,其他的进程还要继续进入休眠状态;如此循环。
为了解决这个问题,内核提供了互斥的等待(exclusive wait)选项,和普通的休眠有以下区别:
  1. 设置WQ_FLAG_EXCLUSIVE标志的进程,在添加到等待队列时置于队尾,而没有设置该标志位的会置于队首。
  2. 调用wake_up函数时,在唤醒第一个设置WQ_FLAG_EXLCUSIVE的进程后停止执行
wake函数的变体可以参阅文件linux/wait.h。
 
poll and select

采用非阻塞I/O的程序通常采用poll,select,epoll系统调用来判断是否可以对一个打开的文件进行非阻塞的读写;这些系统调用也可以将进程阻塞,直到文件可以读写。
驱动程序需要实现poll函数来支持这些系统调用:
unsigned int (*poll) (struct file *filp, poll_table *wait);
驱动程序的poll函数通常包含两个步骤:
  1. 对表明poll状态改变的等待队列调用poll_wait,如果此时I/O没有可用的文件描述符,内核使进程进入等待状态
  2. 返回一个指明可以立刻执行的不被阻塞的操作的掩码
struct poll_table定义在linux/pull.h,驱动可以调用poll_wait,将一个等待队列添加到poll_table。
void poll_wait(struct file *filep, wait_queue_head_t *wait_queue, poll_table *wait);
操作码的定义也在文件linux/pull.h中。
 
poll和select是为了提前判断I/O操作是否会被阻塞,作为读写操作的补充。三个操作的实现,对于驱动程序的正常工作十分重要,总结起来:
  1. 读操作:
    • 如果缓冲区有数据,立即返回;poll报告POLLIN|POLLRDNORM
    • 如果缓冲区没有数据,默认情况下阻塞,直到有数据到来;如果O_NONBLOCK标志设置,立即返回-EAGAIN。poll报告当前设备不可读
    • 如果到达文件末尾,返回0,无论是否设置O_NONBLOCK;poll报告POLLHUP
  2. 写操作:
    • 如果缓冲区有空间,立即返回;poll报告POLLOUT|POLLWRNORM
    • 如果缓冲区已满,默认阻塞写操作,直到有空间可用;如果设置O_NONBLOCK,立即返回-EAGAIN,poll报告文件不可写。如果设备不能接收更多的数据,无论O_NONBLOCK是否设置,返回-ENOSPC
    • "Never make a write call wait for data transmission before returning, even if O_NONBLOCK is clear."这是因为许多程序调用select来判断写操作是否会被阻塞,如果设备被报告是可写的,调用不能被阻塞
  3. Flushing pending output:
    • 系统调用fsync会调用设备驱动的fsync很熟,将数据写入到设备中
int (*fsync) (struct file *file, struct dentry *dentry, int datasync);
用户程序调用pollselect、或者epoll_ctl时,内核会调用所有该系统调用引用的文件的poll方法( the kernel invokes the poll method of all files referenced by the system call),传递相同的poll_table参数。poll_table是poll_table_entry的链表,每个链表项包含一个struct file和一个指向wait_queue_head_t指针。整个链表由内核维护。
poll函数返回时,poll_table会被销毁,所有添加到等待队列的项也会被删除。
如果系统中的文件过多,在调用poll函数时需要生成巨量的数据结构,函数完成后又会释放,导致开销十分大。epoll族函数能够一次创建这个内核数据结构,使用多次。
 
Asynchronous Notification

考虑以下场景:一个优先级较低的应用要执行一个周期很长的计算操作,还要尽可能快的处理新到的数据。如果采用阻塞的I/O,由于其优先级较低,可能等待的事件很长。如果采用轮询的方法,需要定时地调用poll函数,性能开销大。
异步通知的方法,能够实现程序需要的数据可用时,收到一个信号将其唤醒,尽可能快的进入运行状态。
只需两步即可激活异步通知功能:
  1. 将一个进程设为要接收通知的文件的所有者:调用fcntl系统调用将进程的ID保存到filp->f_owner,告知内核文件有新数据到来时应该通知谁
  2. 通过fcntl系统调用设置设备文件的FASYNC标志
这样,新数据到来时,内核会发送SIGIO信号给相应的进程。
通常情况下,socket和tty支持异步通知。如果某个进程在多个文件上都激活了异步通知功能,还需要调用poll或者select函数来确定发出通知的文件。
 
驱动程序要实现异步通知功能,可以借助于内核linux/fs.h中提供的函数:
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
void kill_fasync(struct fasync_struct **fa, int sig, int band);

 

llseek

为了保证读写函数能够正常工作,驱动程序还需要提供llseek函数。如果设备不支持seek操作,例如串口设备,需要在驱动的open方法里调用nonseekable_open,
int nonseekable_open(struct inode *inode, struct file *filp);
将传入filp标记为nonseekable,并在file_operations中的llseek域设为no_llseek辅助函数。
 
Access Control on a Device File

只能由一个进程使用的设备文件:在驱动程序的open函数内添加一个原子变量来控制使用该文件的进程的数量。
只能由一个用户使用的设备文件:在设备的数据结构内添加用户的ID和设备打开的计数
虚拟设备:对于没有绑定到特定硬件对象的设备,可以创建一个软件的副本
 
 
志:Linux将所有设备都抽象为文件进行管理,于是对于设备权限的管理就是对于相对应文件的权限的管理。
      系统调用是操作系统提供给用户的与内核交互的接口,而驱动是和设备交互的接口——于是用户程序调用系统调用时,最后都会由内核发送给驱动程序进行处理,调用驱动程序对应的函数。
posted @ 2018-11-09 17:54  glob  阅读(161)  评论(0编辑  收藏  举报