技术交流

好好学习,天天向上。

0%

第六章——高级字符驱动程序操作

介绍

本章涵盖ioctl、poll、select、llseek以及异步通知等操作。这篇笔记记得比较粗略,但是这没有什么关系。学习编程技术类的知识,最重要的是知道有某种方法可以实现某个功能,而不是细致到把每个遇到的功能都记在脑子里。

IOCTL

ioctl(io control),是struct file_operations中用于文件操作的接口之一(新版内核有改变,CentOS8 4.18内核中没有ioctl了,取而代之的是unlocked_ioctl和compat_ioctl),从这个角度来看虚拟文件系统中的文件都可以有ioctl操作。ioctl操作提供了命令扩展的机制,让驱动更灵活。前面的章节讲到,可以在ioctl中封装查询式调试命令。实时上ioctl还有其他的妙用,比如某个设备可以是字符流作为命令,也可以是ioctl作为控制命令。一般而言ioctl如果无法匹配到命令,返回-EINVAL是合理的,但是posix标准说应该返回-ENOTTY。

ioctl提供给用户态的接口,第三个参数是可变参数。但实际上ioctl只接收三个参数,不能将其理解为可接收任意参数。用于ioctl的部分命令有部分是系统内定的,可以查阅内核文档查看。内核将用于ioctl的命令分解为4个具有不同意义的字段看待,分别是type、number、direction以及size。其含义如下表

字段名称 位宽 含义
type 8位 一个企图用来区分不同驱动模块的值
number 8位 命令的序号,多个命令时,一般让其自然数排列
direction 1位 表征该命令执行时数据传输方向
size 通常是13或者14位,通过_IOC_SIZEBITS查看 表征该命令执行时数据传输大小

为了避免不明确的、冲突的命令值定义,内核提供了一套宏来辅助用户定义用于ioctl的宏。注意,这里的写入还是读取是对于驱动模块的使用者而言的。

1
2
3
4
_IO(type, number)              // 生成一个无参数命令
_IOW(type, number, datatype) // 生成一个写入命令,命令的size是sizeof(datatype)
_IOR(type, number, datatype) // 生成一个读取命令
_IOWR(type, number, datatype) // 生成一个读写命令

同时内核还提供了一套宏,来验证用户传入的命令是否和驱动定义的驱动匹配,命令是否有效

1
2
3
_IOC_TYPE(type) // 从命令中提取type
_IOC_NR(type) // 从命令中提取number
_IOC_DIR(cmd) // 从命令中提取方向,应该是这两个者之一:_IOC_READ,_IOC_WRITE

阻塞I/ O

阻塞I/O是指当驱动/设备不能满足用户进程的需求是,用户进程可能陷入等待的I/O请求。当驱动/设备无法满足用户请求时,可以让用户进程进入休眠状态。此处再次强调:1、不能在原子上线中休眠;2、不能在有自旋锁、seqlock或者RCU时休眠;3、禁用了中断时不能休眠。持有信号量时是可以睡眠的,但是要考虑后果。用户进程休眠依赖于wait_queue_head_t数据结构。内核的睡眠模型是这样的,当用户向驱动请求某项资源而不可得的时候,驱动将用户进程睡眠到wait_queue_head_t数据结构上。当其他的内核代码让请求的资源变得可用时(释放或创建),其他内核代码(可能是同一份代码的重入)负责通过同一个wait_queue_head_t的指针唤醒休眠的用户进程。用户进程醒来时需要自己判断醒来是再次休眠 还是往下走。

休眠API

常用的休眠 API如下所示,注意下方的wait_queue_t在最新的内核中已经改成了wait_queue_entry

1
2
3
4
5
6
7
8
9
10
11
12
13
DECLARE_WAIT_QUEUE_HEAD(queue);    // 一步到位地创建病初始化一个等待队列头,等待队里头用队列和自旋锁实现
wait_queue_head_t queue; // 拆分的等待队列头创建
init_waitqueue_head(&queue); // 拆分的等待队列头初始化

// 注意下方宏queue是指传递,condition是不能有副作用的表达式,该表达式可能被多次求值
wait_event(queue, condition) // 让进程挂在queue上,指导condition表达式为真的时候才跳出。
wait_event_interruptible(queue, condition) // 让进程进入可打断休眠,如果休眠被打断,则返回非零值。此时驱动应返回-ERESTARTSYS
wait_event_timeout(queue, condition, timeout)// 让进程进入带超时的休眠,发超时或打断返回0
wait_event_interruptible_timeout(queue, condition, timeout) // 让进程进入带超时且可打断的睡眠,发生超时或打断返回0
// 函数,用于实现细粒度的休眠
void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state);
void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state); //独占式休眠
void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait); //反休眠,将wait从queue中移除

wait_eventxxx还可以手动的实现,经典代码如下所示

1
2
3
4
5
6
7
while(!condition) {
DEFINE_WAIT(wait);
prepare_to_wait(&queue, &wait, TASK_INTERRUPTIBLE);
if (!condition)
schedule( );
finish_wait(&queue, &wait);
}

某些进程设置了struct file(一次打开的上下文)指针flip->f_flags中的O_NONBLOCK标志。该标志可以是打开时指定,也可以是fcntl设定。如果驱动发现用户设置了该标志,则执行open、read、write时不应该将用户进程休眠。

唤醒

有时候驱动代码需要实现更加细粒度的休眠和唤醒,例如当资源只有一个的时候,就不需要将休眠在同一个quque上的进程一次性都唤醒。驱动可以将进程置于独占式休眠状态,这样唤醒时就可以仅唤醒一个独占休眠进程让其独占资源。独占式休眠进程的休眠item会被放到休眠queue的尾部,而非独占式休眠的item会被放到queue的头部。

1
2
3
4
5
6
7
8
9
10
11
12
wake_up(wait_queue_head_t *queue);               // 唤醒wait_queue_head_t上的所有非独占进程
wake_up_interruptible(wait_queue_head_t *queue); // 唤醒wait_queue_head_t上那些非独占且可打断休眠的进程

wake_up_nr(wait_queue_head_t *queue, int nr); // 唤醒指定数量的(nr个)的版本
wake_up_interruptible_nr(wait_queue_head_t *queue, int nr);

wake_up_all(wait_queue_head_t *queue); // 忽略独占状态的版本
wake_up_interruptible_all(wait_queue_head_t *queue);

// 下面版本禁止wake的目标进程醒后马上抢占wake发起者的CPU,可用于wake发起者wake目标进程后,还仅
// 遗留部分短暂工作就会主动释放CPU了的情况。
wake_up_interruptible_sync(wait_queue_head_t *queue);

poll和select

unix派生了不同分支,再加上linux的出现。内核中经常有这样的情况,那就是不同的发行版做了相同的功能。poll和select就是如此,它们想解决的核心问题就是如何让进程阻塞在多个文件描述符上。下面是我收集到的不同的poll和select版本,它们功能大同小异,具体用法可以再查询man手册。

1
2
3
4
5
6
7
8
9
int epoll_create(int size);
int epoll_create1(int flags);

int poll (struct pollfd *fds, unsigned int nfds, int timeout);
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);

用户态的这些接口需要内核态的支持。所有的poll和select在驱动层面都是由下面这个接口作为基础的,它是file_operations的成员之一。

1
unsigned int (*poll)(struct file *filp, poll_table *wait);

这个接口主要干两件事情

  • 当驱动/设备无法满足进程需求的时候,将进程休眠到文件描述符对应的休眠队列上
  • 当驱动/设备可以满足进程需求的时候,返回对应的标志位,告知用户进程

异步通知

异步通知和poll思路刚好反过来。poll有点像轮询思路,是主动询问,而异步通知是一种让驱动主动给进程发信号的设计。在用户态,用户需要设定接受信号的进程。在内核态,驱动需要在资源到达时主动向进程发消息。实现这个功能的经典用户态代码如下

1
2
3
4
signal(SIGIO, &input_handler); /* 注册信号回调函数,其实最好用sigaction实现 */
fcntl(STDIN_FILENO, F_SETOWN, getpid( )); // 告知驱动,将消息发给哪个pid
oflags = fcntl(STDIN_FILENO, F_GETFL);
fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC); // 设置FASYNC,让驱动有事情立马报告

在驱动层面,驱动主要做下面这些事情

1
2
3
4
5
6
7
8
9
10
11
// 当F_SETOWN调用时,设置filp->f_owner,这一步虚拟文件系统负责

// 当F_SETFL调用时,调用fasync方法。事实上,fasync也有固定模板,一切交给fasync_helper就可以了
static int scull_p_fasync(int fd, struct file *filp, int mode) {
struct scull_pipe *dev = filp->private_data;
return fasync_helper(fd, filp, mode, &dev->async_queue);
}

// 当数据ready时,向用户进程发信号
if (dev->async_queue)
kill_fasync(&dev->async_queue, SIGIO, POLL_IN);

llseek

llseek就是拿来调整文件读写位置的,按照参数格式调整filp->f_ops