知识点:进程休眠及唤醒,如何实现非阻塞IO,设备可读取或写入时通知用户空间。
int ioctl(int fd, unsigned long cmd, …);
驱动程序ioctl原型
int (*ioctl)(struct inode inode, struct file filp, unsigned int cmd, unsigned long arg);
如果调用程序没有传递第三个参数,那么驱动程序所接收的arg参数就处于未定义状态。
type 魔数 Documentation/ioctl-number.txt 定义内核使用的魔数,驱动程序可以选择其他的避免冲突
number序号,
direction
size
任何一个系统调用返回时,正的返回值是受保护的。
一些命令编号是由内核标识的,我们的编号不能和他相同,否则,他们会在我们自己的文件操作被调用之前被解码,而且由于ioctl编号冲突,应用程序的结果将无法预测。
预定义命令分为三组:
可用于任何文件(普通,设备,FIFO和套接字)的命令
只能用于普通文件
特定于文件系统类型的命令
FIOCLEX
FIONCLEX
FIOASYNC
FIOQSIZE
FIONBIO
ioctl和fcntl的不同主要是由于历史原因造成的:unix的开发人员面对控制IO操作的问题时,他们认为文件和设备是不同的。那是,与ioctl实现相关的唯一设备就是中断,这也解释了为什么肥大的ioctl命令的标准返回值是-ENOTTY。
对于附加参数,如果它是个整数,那么可以直接使用,如果它是个指针,在使用之前要检查合法性。
ioctl处理小数据项,可以使用access_ok 和put_user、__put_user、get_user、__get_user来替代copy_to_user、copy_from_user
需要注意的是access_ok没有完成内存验证的全部工作,大多数驱动程序中不需要真正调用access_ok
对设备的访问由设备文件的权限控制,驱动程序通常不进行权限检车。
通过使用capable接口可以对进程调用ioctl时进行权限检查。
使用ioctl时参数交换方式主要有:
通过指针设置
通过值设置,
通过指针获取
通过值获取
通过指针交换
通过值交换
通常情况下应该保持一致,要么使用指针,要么使用值,避免混用。
通过向设备写入控制序列控制设备,而不是通过ioctl。这种方式适用于不传递数据而只响应命令的设备。
另外一种思路是驱动程序附带一个用户态命令行工具,负责在用户态处理复杂序列,并把它发送给驱动程序。
进程休眠的两天原则:
第一,永远不要在原子上下文中进入休眠:不能在持有spinlock,seqlock,rcu的情况进行休眠,在semephore中休眠是合法的,但是要确保休眠时间短,且不阻塞最终唤醒自己的那个线程。
第二,对唤醒之后的状态不做假定,无法知道休眠了多长时间,或者其他进程是否被唤醒,以及等待的资源是否可用。
等待队列,wait_queue_head_t DECLARE_WAIT_QUEUE_HEAD init_waitqueue_head
wait_event
wait_event_interruptible 非零值表示休眠被某个信号中断,而驱动程序也需要返回-ERESTARTS
wait_event_timeout
wait_event_interruptible_timeout
event通过值传递,condition可能会多次求值,并且会在休眠前后都执行,所以不能传递有副作用的表达式,timeout版本只会等待给定时间,时间到期时返回0,不管condition如何求值。
wake_up
wake_up_interruptible 只会唤醒执行可中断休眠的进程 interruptible版本对应interruptible版本
多个休眠进程等待相同的condition,但是这些休眠进程不能并发执行时,condtion必须以原子方式进行检查
驱动程序中实现缓冲区可以提高性能,这得益于减少了上下文切换和用户级/内核级转换的次数。
如果指定了O_NONBLOCK标志,read和write操作在数据没有就绪时,则返回-EAGAIN错误。很容易把一个非阻塞返回错误认为是EOF,所以必须始终检查errno
open操作可能会在如下场景阻塞:
初始化过程很长
打开一个还没有进程向其中写入的FIFO
访问一个被锁住的磁盘
磁盘没有插入时打开一个磁带设备
对wait_event使用的一个不错的代码模板
高级休眠
初始化一个wait_Queue_t结构,由自旋锁保护
将进程加入等待队列中
设置进程状态为TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE,不建议直接修改current的属性,避免实现发生修改之后代码需要修改
再次检查休眠等待条件,避免错过wakeup
如果条件为假,执行schedule让出CPU
条件不足跳过等待或者从schedule返回之后
重置状态为TASK_RUNNING
从等待队列中移除。
早起linux工作方式,和上面步骤对应的:
DEFINE_WAIT
prepare_wait 作用就是加入等待队列,设置进程状态
同样需要检查等待条件
flush_wait最后执行,设置进程状态,从等待队列中移走
需要说明的是,检查等待条件是不可缺少的一个步骤,否则可能导致进程无法被唤醒。虽然检查等待条件和执行schedule操作不是一个原子操作,但是此时进程已经加入了等待队列中,所以即使判断等待条件和执行schedule之间发生了切换,唤醒操作仍然能够在等待队列中找到该进程,而再执行schedule的时候,进程不会发生休眠,他的状态已经被修改为RUNNING了,但是如果我们不执行这个检查条件的操作,那么另外一个进程在把当前进程加入等待队列之前就执行了唤醒操作,而此时进程不在等待队列中,无法设置状态,在后点执行schedule时进程会进入休眠状态,有可能永远无法唤醒。
疯狂兽群问题,wakeup操作唤醒等待队列中的所有进程,而这些进程会为资源竞争,最后只有一个进程执行,其他进程再次进入休眠。这样会严重影响系统性能。
通过独占等待选项来解决这个问题。
有独占等待标记的进程被放入尾部,而没有独占等待标记的被放到头部。唤醒操作只唤醒第一个设置了独占标记的进程。不会唤醒其他设置了独占标记的进程,但是会唤醒其他没有设置独占标记的进程。
使用独占等待应该具备两个条件:对某个资源存在严重竞争,并且唤醒单个进程就能完整消耗该资源。
函数 prepare_to_wait_exclusive 替代prepare_to_wait
wake_up
wake_up_interruptible
wake_up_nr
wake_up_interruptible_nr 唤醒多少个独占等待进程,0表示所有
wake_up_all
wake_up_interruptibel_all 唤醒所有独占或非独占,不可中断的除外
wake_up_interruptibel_sync wake_up操作不是原子的,如果不在自旋锁保护下,有可能在函数返回之前就被调出了cpu,使用sync版本可以避免这个被调出。
建议不使用,因为sleep_on没有任何竞态保护,窗口期间出现的唤醒可能会丢失导致无法唤醒。
允许进程决定是否可以对一个或多个打开的文件做非阻塞的读取或写入。这些调用也会则色进程,知道给定文件描述符集合中的任何一个可以读取或写入。
poll和select调用的目的是确定接下来的IO操作是否会阻塞。从这个方面来说,他们是read和write的补充。更重要的用途是可以使应用程序同时等待多个数据流。
设备方法分为两步处理:
在poll调用结束时,poll_table结构被重新分配,因此如果涉及到的文件描述符过多就会机器浪费,epoll系统调用族可以构造一次重复使用。