Linux驱动开发进阶(五)- 系统调用
文章目录
- 1、前言
- 2、阻塞与非阻塞IO
- 2.1、阻塞方式
- 2.2、非阻塞方式
- 2.3、小结
- 3、异步IO
- 3.1、poll
- 3.2、select
- 3.3、epoll
- 3.4、poll和epoll示例比较
- 3.5、异步通知
- 4、unlocked_ioctl
- 5、sysfs_notify
1、前言
- 学习参考书籍以及本文涉及的示例程序:李山文的《Linux驱动开发进阶》
- 本文属于个人学习后的总结,不太具备教学功能。
2、阻塞与非阻塞IO
2.1、阻塞方式
用户程序调用read
系统调用时,从用户态切换到内核态。内核检查设备是否准备好数据,如果设备尚未准备好数据,内核会将当前进程标记为“阻塞”状态,并将其放入等待队列中,同时进入休眠状态。当设备准备好数据时,内核会将等待队列中的进程唤醒。内核从设备读取数据,并将数据复制到用户空间。系统调用返回,用户程序继续执行。
相关示例程序可以参考:https://gitee.com/li-shan-asked/linux-advanced-development-code/tree/master/part5/block_io
重点宏或函数如下:
init_waitqueue_head(wq_head)
wait_event_interruptible(wq_head, condition)
wake_up_interruptible(x)
2.2、非阻塞方式
非阻塞方式和阻塞方式相比,当用户程序调用read
系统调用时,如果设备尚未准备好数据,直接返回错误。此时可以继续发起read
系统调用。
相关示例程序可以参考:https://gitee.com/li-shan-asked/linux-advanced-development-code/tree/master/part5/nonblokck-io
2.3、小结
所以阻塞方式不会占用cpu资源,适合对实时性不高的场景。非阻塞方式需要不断轮询,可能会增加cpu负载,适合高并发场景。
3、异步IO
3.1、poll
poll可以说是对阻塞IO的一种改进。我们知道阻塞IO中,如果设备状态一直不可用,那么应用程序会一直休眠。而poll机制中,就是增加了超时机制。当一段时间内设备还是不可用,则强行调度,将应用程序唤醒。
作为驱动开发者,需要实现poll驱动的回调函数。
static __poll_t key_poll(struct file *file, poll_table *wait)
{
poll_wait(file, &key->wait_head, wait);
if (key->ev_press == 1)
return EPOLLIN | EPOLLRDNORM;
return EPOLLOUT | EPOLLWRNORM;
}
static struct file_operations key_ops = {
...
.poll = key_poll,
...
};
相关示例程序可以参考:https://gitee.com/li-shan-asked/linux-advanced-development-code/tree/master/part5/poll
3.2、select
poll机制和select机制是一样的,应用程序调用select系统调用时,最终调用的还是内核驱动程序中的poll函数。所以主要了解应用程序中的select系统调用即可。
相关示例程序可以参考:https://gitee.com/li-shan-asked/linux-advanced-development-code/tree/master/part5/select
3.3、epoll
当fd数量小于1000时,使用poll和select还算合适。因为poll和select每次调用时都需要遍历所有监听的fd,检查他们的就绪状态,例如,监听10000个fd,即使只有1个fd就绪,也需要遍历全部10000个,效率极低。
而epoll适用于有大量的描述符需要同时轮询,并且这些连接最好是长连接。用户空间的应用程序调用epoll相关接口,最终还是调用内核的poll接口。
相关示例程序可以参考:https://gitee.com/li-shan-asked/linux-advanced-development-code/tree/master/part5/epoll
3.4、poll和epoll示例比较
poll的低效实现:
// 每次需遍历所有 FD
struct pollfd fds[10000];
while (1) {
int ret = poll(fds, 10000, 1000); // O(n) 遍历
for (int i = 0; i < 10000; i++) { // 再次遍历检查
if (fds[i].revents & POLLIN) {
// 处理就绪 FD
}
}
}
epoll的高效实现:
int epfd = epoll_create1(0);
struct epoll_event ev, events[100];
// 注册 FD(首次拷贝到内核)
ev.events = EPOLLIN | EPOLLET; // ET 模式
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
while (1) {
int nready = epoll_wait(epfd, events, 100, 1000); // 仅返回就绪 FD
for (int i = 0; i < nready; i++) { // 直接处理
// 处理 events[i].data.fd
}
}
epoll是Linux特有的,注意跨平台限制。
3.5、异步通知
上面介绍的poll/select、epoll还是需要主动去查询设备是否可用,大多数情况下,设备都是不可用的。所以是否还有更好的方式,那就是异步通知,当设备可用时,内核驱动程序主动向应用程序发起通知。
可以参考以前写的:I/O管理:异步通知
同时相关示例程序可以参考:https://gitee.com/li-shan-asked/linux-advanced-development-code/tree/master/part5/async
4、unlocked_ioctl
ioctl
是旧版本的内核API,需要持有大内核锁。unlocked_ioctl
是现代内核中推荐使用的API,它不需要持有大内核锁,从而提高了性能和灵活性。
相关示例程序可以参考:https://gitee.com/li-shan-asked/linux-advanced-development-code/tree/master/part5/ioctl
5、sysfs_notify
当用户空间使用poll、select、epoll接口检测一个sysfs目录下的属性文件时,此时如果内核不做任何动作,则用户空间中的进程就会被阻塞起来,当内核中调用sysfs_notify函数时,此时用户空间的进程被唤醒,就可以执行相应的动作。
sysfs_notify函数实现如下:
最终会调用kernfs_notify(),最终调用wake_up_interruptible:
相关示例程序可以参考:https://gitee.com/li-shan-asked/linux-advanced-development-code/tree/master/part5/sysfs_notify