阻塞与非阻塞的概念:
阻塞IO: 当数据不可读或不可写,进程休眠,直到得到数据可读或可写时才返回。阻塞效率高,实时性比较好。
非阻塞IO:不管数据是否可读可写,都马上返回。
进程和线程
我们先从 Linux 的进程谈起,操作系统要运行一个可执行程序,首先要将程序文件加载到内存,然后 CPU 去读取和执行程序指令,而一个进程就是“一次程序的运行过程”,内核会给每一个进程创建一个名为task_struct的数据结构,而内核也是一段程序,系统启动时就被加载到内存中了。
进程在运行过程中要访问内存,而物理内存是有限的,比如 16GB,那怎么把有限的内存分给不同的进程使用呢?跟 CPU 的分时共享一样,内存也是共享的,Linux 给每个进程虚拟出一块很大的地址空间,比如 32 位机器上进程的虚拟内存地址空间是 4GB,从 0x00000000 到 0xFFFFFFFF。但这 4GB 并不是真实的物理内存,而是进程访问到了某个虚拟地址,如果这个地址还没有对应的物理内存页,就会产生缺页中断,分配物理内存,MMU(内存管理单元)会将虚拟地址与物理内存页的映射关系保存在页表中,再次访问这个虚拟地址,就能找到相应的物理内存页。每个进程的这 4GB 虚拟地址空间分布如下图所示:
进程的虚拟地址空间总体分为用户空间和内核空间,低地址上的 3GB 属于用户空间,高地址的 1GB 是内核空间,这是基于安全上的考虑,用户程序只能访问用户空间,内核程序可以访问整个进程空间,并且只有内核可以直接访问各种硬件资源,比如磁盘和网卡。那用户程序需要访问这些硬件资源该怎么办呢?答案是通过系统调用,系统调用可以理解为内核实现的函数,比如应用程序要通过网卡接收数据,会调用 Socket 的 read 函数:
ssize_t read(int fd,void *buf,size_t nbyte)
CPU 在执行系统调用的过程中会从用户态切换到内核态,CPU 在用户态下执行用户程序,使用的是用户空间的栈,访问用户空间的内存;当 CPU 切换到内核态后,执行内核代码,使用的是内核空间上的栈。
从上面这张图我们看到,用户空间从低到高依次是代码区、数据区、堆、共享库与 mmap 内存映射区、栈、环境变量。其中堆向高地址增长,栈向低地址增长。
请注意用户空间上还有一个共享库和 mmap 映射区,Linux 提供了内存映射函数 mmap, 它可将文件内容映射到这个内存区域,用户通过读写这段内存,从而实现对文件的读取和修改,无需通过 read/write 系统调用来读写文件,省去了用户空间和内核空间之间的数据拷贝,Java 的 MappedByteBuffer 就是通过它来实现的;用户程序用到的系统共享库也是通过 mmap 映射到了这个区域。
我在开始提到的task_struct结构体本身是分配在内核空间,它的vm_struct成员变量保存了各内存区域的起始和终止地址,此外task_struct中还保存了进程的其他信息,比如进程号、打开的文件、创建的 Socket 以及 CPU 运行上下文等。
在 Linux 中,线程是一个轻量级的进程,轻量级说的是线程只是一个 CPU 调度单元,因此线程有自己的task_struct结构体和运行栈区,但是线程的其他资源都是跟父进程共用的,比如虚拟地址空间、打开的文件和 Socket 等。
阻塞与唤醒
我们知道当用户线程发起一个阻塞式的 read 调用,数据未就绪时,线程就会阻塞,那阻塞具体是如何实现的呢?
Linux 内核将线程当作一个进程进行 CPU 调度,内核维护了一个可运行的进程队列,所有处于TASK_RUNNING状态的进程都会被放入运行队列中,本质是用双向链表将task_struct链接起来,排队使用 CPU 时间片,时间片用完重新调度 CPU。所谓调度就是在可运行进程列表中选择一个进程,再从 CPU 列表中选择一个可用的 CPU,将进程的上下文恢复到这个 CPU 的寄存器中,然后执行进程上下文指定的下一条指令。
而阻塞的本质就是将进程的task_struct移出运行队列,添加到等待队列,并且将进程的状态的置为TASK_UNINTERRUPTIBLE或者TASK_INTERRUPTIBLE,重新触发一次 CPU 调度让出 CPU。
那线程怎么唤醒呢?线程在加入到等待队列的同时向内核注册了一个回调函数,告诉内核我在等待这个 Socket 上的数据,如果数据到了就唤醒我。这样当网卡接收到数据时,产生硬件中断,内核再通过调用回调函数唤醒进程。唤醒的过程就是将进程的task_struct从等待队列移到运行队列,并且将task_struct的状态置为TASK_RUNNING,这样进程就有机会重新获得 CPU 时间片。
这个过程中,内核还会将数据从内核空间拷贝到用户空间的堆上。
当 read 系统调用返回时,CPU 又从内核态切换到用户态,继续执行 read 调用的下一行代码,并且能从用户空间上的 Buffer 读到数据了。
小结
今天我们谈到了一次 Socket read 系统调用的过程:首先 CPU 在用户态执行应用程序的代码,访问进程虚拟地址空间的用户空间;read 系统调用时 CPU 从用户态切换到内核态,执行内核代码,内核检测到 Socket 上的数据未就绪时,将进程的task_struct结构体从运行队列中移到等待队列,并触发一次 CPU 调度,这时进程会让出 CPU;当网卡数据到达时,内核将数据从内核空间拷贝到用户空间的 Buffer,接着将进程的task_struct结构体重新移到运行队列,这样进程就有机会重新获得 CPU 时间片,系统调用返回,CPU 又从内核态切换到用户态,访问用户空间的数据。
应用程序是否能实现阻塞或非阻塞是取决于驱动程序。实际驱动中应该把阻塞和非阻塞这种选择权交给应用程序来选择。要实现这个效果 ,就必须让驱动程序知道应用程序的选择。这个信息是通过 file 结构来传递的。
struct file 结构中有成员:
unsigned int f_flags;
存放的就是 open(path,flag);
中的flag参数。
驱动中每个接口函数都有一个file指针,通过这个参数就能取得应用程序打开方式(阻塞或非阻塞)
等待队列
内核对这种阻塞提供等待队列机制来实现,这样可以改善实时性问题。
等待队列头数据结构
内核使用这个结构来给进程一个休眠的地方。
定义如下:Wait.h (include\linux)
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
1.等待队列的睡眠
wait_event(wq, condition) ;建立不可以杀进程(信号不能唤醒,效果和msleep相同)。
wait_event_interruptible(wq, condition) ;它可以被被信号唤醒。休眠过程中,进程可以接收信号,收到后不管条件如何,直接返回。
wait_event_timeout(wq, condition, timeout) ;休眠期间效果和 wait_event ,但是有一个超时时间 ,时间到,不管条件如何,直接返回。
wait_event_interruptible_timeout(wq, condition, timeout);休眠期间效果和 wait_event_interruptible相同,区别是有超时功能。时间 到,不管条件如何,直接返回。
wq 是变量
2.等待队列的唤醒
通过调用 wake_up* 函数(表面上是宏)唤醒进程
wake_up(wq) 能用于唤醒各种方式进入休眠的进程,只唤醒队列上的一个进程。
wake_up_all(wq)效果和wake_up相同,只是能唤醒队列上所有的进程。
wake_up_interruptible(wq) 只能用于唤醒一个 使用wait_event_interruptible*休眠的进程。
wake_up_interruptible_all(wq)能唤醒队列所有 使用wait_event_interruptible*休眠的进程。
wq 是指针。
以上函数并不是说一调用,就一定可以把进程唤醒,其实它会先去检测进程休眠的条件,是否变成真了,如果为真才把它唤醒。
3. 等待队列 API
1)定义一个等待不队列头变量 ,并且 初始化。有两种方法:
方法1:动态定义:
wait_queue_head_t wq; //全局变量
在模块初始化函数中调用:
init_waitqueue_head(&wq); //安装模块时候执行了初始化
方法2:静态初始化
DECLARE_WAIT_QUEUE_HEAD(wq) ; //这句等效于上面两句。
DECLARE_WAIT_QUEUE_HEAD(name) 表示定义一个 名字为name 的等待队列头变量,并且进行初始化。
2)在需要休眠的地方调用 wait_event*类宏让进程休眠
示例:
在按键驱动代码基础上修改read函数的休眠代码
/* 按键驱动read接口 */
static int buttons_read(struct file *filp,
char __user *buff,
size_t count,
loff_t *offp)
{
unsigned long err;
/* count ==0 ,则返回0,不是错误 */
if(!count) {
return 0;
}
/* 没有按键动作 */
if ( press==0 ) {
if ( filp->f_flags & O_NONBLOCK) {
return -1;
}
else { /* 用户以阻塞方式打开 */
wait_event_interruptible(wq, press);
}
}
/*清按键动作标记*/
press = 0;
/* 修正大小 */
count = min(sizeof(key_values), count);
/* 复制按键缓冲数据给用户空间 */
err = copy_to_user((void __user *)buff, (const void *)(&key_values), count);
if(err) {
return -EFAULT;
}
return count;
}
3)在需要唤醒的地方(一般是要中断程序中)把 休眠条件设置真,然后调用 wake_up* 类宏唤醒进程。
接上一步修改 中断服务程序:
在原来的 press = 1 ;
后面添加 wake_up_interruptible 代码,如下。
press = 1; //设置按键动作标志位
// wake_up(&wq) ; //唤醒进程
wake_up_interruptible(&wq) ; //唤醒进程
现在要解决的问题是给应用程序提供一个接口,告诉它是否有按键动作发生了。这个接口就是驱动 中文件操作结构中的 poll 接口。
linux 驱动 poll 接口---------- 这个接口是用来告诉应用程序本设备是否可以读,或是否可写。
unsigned int xxxx_poll( struct file *file,struct poll_table_struct *wait)
poll 接口模板 :
1) 定义一个等待队列头,并且 初始化
2)按照以下的代码模板实现poll接口。
//静态定义名字是button_waitq的等待队列头, 并且对它进行初始化
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
/* 按键驱动poll接口 */
static unsigned int buttons_poll( struct file *file,struct poll_table_struct *wait)
{
unsigned int mask = 0;
poll_wait(file, &button_waitq, wait); //把等待队列加到查询表中,固定方式
//press 非0表示有按键动作
if (press)
mask |= POLLIN | POLLRDNORM;
//如存在一个变量wr_flag表示设备可以写,则是mask |= POLLOUT| POLLWRNORM
/* if(wr_flag)
mask |= POLLOUT| POLLWRNORM;
*/
return mask;
}
poll 的系统调用接口 select()函数
一、驱动实现select机制的步骤
1、首先初始化一个等待队列头
2、在驱动中实现poll函数,该函数只需做两件事情
a、使用poll_wait()函数将等待队列添加到poll_table中。
b、返回描述设备是否可读或可写的掩码。
3、在驱动的相应地方调用wake_up()函数,唤醒等待队列。
两点说明:
a、等待队列
select函数阻塞的原理,实际上是通过等待队列实现的,若对等待队列不熟悉,请看我的另一篇文章《等待队列的简单使用》。否则看以下的 “select机制内核代码走读” 会很吃力。
b、掩码值及含义
POLLIN
如果设备可被不阻塞地读, 这个位必须设置.
POLLRDNORM
这个位必须设置, 如果"正常"数据可用来读. 一个可读的设备返回( POLLIN|POLLRDNORM ).
POLLRDBAND
这个位指示带外数据可用来从设备中读取. 当前只用在 Linux 内核的一个地方( DECnet 代码 )并且通常对设备驱动不可用.
POLLPRI
高优先级数据(带外)可不阻塞地读取. 这个位使 select 报告在文件上遇到一个异常情况, 因为 selct 报告带外数据作为一个异常情况.
POLLHUP
当读这个设备的进程见到文件尾, 驱动必须设置 POLLUP(hang-up). 一个调用 select 的进程被告知设备是可读的, 如同 selcet 功能所规定的.
POLLERR
一个错误情况已在设备上发生. 当调用 poll, 设备被报告位可读可写, 因为读写都返回一个错误码而不阻塞.
POLLOUT
这个位在返回值中设置, 如果设备可被写入而不阻塞.
POLLWRNORM
这个位和 POLLOUT 有相同的含义, 并且有时它确实是相同的数. 一个可写的设备返回( POLLOUT|POLLWRNORM).
POLLWRBAND
如同 POLLRDBAND , 这个位意思是带有零优先级的数据可写入设备. 只有 poll 的数据报实现使用这个位, 因为一个数据报看传送带外数据.
注:应当重复一下 POLLRDBAND 和 POLLWRBAND 仅仅对关联到 socket 的文件描述符有意义: 通常设备驱动不使用这些标志!
二、以按键驱动为例
驱动代码button.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/init.h>
#include <linux/major.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <asm/uaccess.h>
#include <linux/poll.h>
#include <linux/irq.h>
#include <asm/irq.h>
#include <linux/interrupt.h>
#include <asm/uaccess.h>
#include <linux/platform_device.h>
#include <linux/cdev.h>
#include <linux/miscdevice.h>
#include <linux/sched.h>
#include <linux/gpio.h>
#include <asm/gpio.h>
#define BUTTON_NAME "poll_button"
#define BUTTON_GPIO 140
static int button_major = 0;
static int button_minor = 0;
static struct cdev button_cdev;
static struct class *p_button_class = NULL;
static struct device *p_button_device = NULL;
static struct timer_list button_timer;
static volatile int ev_press = 0;
static volatile char key_value[] = {0};
static int old_value;
static int Button_Irq = 0;
static int flag_interrupt = 1;
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
static irqreturn_t buttons_interrupt(int irq, void *dev_id)
{
if(flag_interrupt) {
flag_interrupt = 0;
old_value = gpio_get_value(BUTTON_GPIO);
mod_timer(&button_timer,jiffies + HZ/100); //启动消抖定时器,消抖时间10ms
}
return IRQ_RETVAL(IRQ_HANDLED);
}
static void button_timer_handle(unsigned long arg)
{
int tmp_value;
tmp_value = gpio_get_value(BUTTON_GPIO);
if(tmp_value == old_value) {
key_value[0] = tmp_value;
ev_press= 1; //有按键按下,唤醒等待队列
wake_up_interruptible(&button_waitq);
}
flag_interrupt = 1;
}
static int button_open(struct inode *inode,struct file *file)
{
Button_Irq = gpio_to_irq(BUTTON_GPIO);
enable_irq(Button_Irq);
if(request_irq(Button_Irq, buttons_interrupt, IRQF_TRIGGER_FALLING, "BUTTON_IRQ", NULL) != 0) {
printk("request irq failed !!! \n");
disable_irq(Button_Irq);
free_irq(Button_Irq, NULL);
return -EBUSY;
}
return 0;
}
static int button_close(struct inode *inode, struct file *file)
{
free_irq(Button_Irq, NULL);
return 0;
}
static int button_read(struct file *filp, char __user *buff, size_t count, loff_t *offp)
{
unsigned long err;
if (filp->f_flags & O_NONBLOCK) {
/*nothing to do*/
//如果没有使用select机制,并且应用程序设置了非阻塞O_NONBLOCK,那么驱动这里就不使用等待队列进行等待。
} else {
wait_event_interruptible(button_waitq, ev_press); //如果应用层没有使用select,直接读的话,这里会阻塞,直到按键按下。如果使用select机制,进来这里时ev_press为真,不会阻塞。
}
err = copy_to_user(buff, (const void *)key_value, min(sizeof(key_value), count));
key_value[0] = 0;
ev_press = 0;
return err ? -EFAULT : min(sizeof(key_value), count);
}
static unsigned int button_poll(struct file *file, struct poll_table_struct *wait)
{
unsigned int mask = 0;
//将等待队列添加到poll_table中
poll_wait(file, &button_waitq, wait);
if(ev_press) {
//返回描述设备是否可读或可写的掩码
mask = POLLIN | POLLRDNORM;
}
return mask;
}
static const struct file_operations button_fops = {
.owner = THIS_MODULE,
.open = button_open,
.release = button_close,
.read = button_read,
.poll = button_poll,
//.write = button_write,
//.ioctl = button_ioctl
};
static int button_setup_cdev(struct cdev *cdev, dev_t devno)
{
int ret = 0;
cdev_init(cdev, &button_fops);
cdev->owner = THIS_MODULE;
ret = cdev_add(cdev, devno, 1);
return ret;
}
static int __init button_init(void)
{
int ret;
dev_t devno;
printk("button driver init...\n");
init_timer(&button_timer);
button_timer.function = &button_timer_handle;
if(button_major) {
devno = MKDEV(button_major, button_minor);
ret = register_chrdev_region(devno, 1, BUTTON_NAME);
} else {
ret = alloc_chrdev_region(&devno, button_minor, 1, BUTTON_NAME);
button_major = MAJOR(devno);
}
if(ret < 0) {
printk("get button major failed\n");
return ret;
}
ret = button_setup_cdev(&button_cdev, devno);
if(ret) {
printk("button setup cdev failed, ret = %d\n",ret);
goto cdev_add_fail;
}
p_button_class = class_create(THIS_MODULE, BUTTON_NAME);
ret = IS_ERR(p_button_class);
if(ret) {
printk(KERN_WARNING "button class create failed\n");
goto class_create_fail;
}
p_button_device = device_create(p_button_class, NULL, devno, NULL, BUTTON_NAME);
ret = IS_ERR(p_button_device);
if (ret) {
printk(KERN_WARNING "button device create failed, error code %ld", PTR_ERR(p_button_device));
goto device_create_fail;
}
return 0;
device_create_fail:
class_destroy(p_button_class);
class_create_fail:
cdev_del(&button_cdev);
cdev_add_fail:
unregister_chrdev_region(devno, 1);
return ret;
}
static void __exit button_exit(void)
{
dev_t devno;
printk("button driver exit...\n");
del_timer_sync(&button_timer);
devno = MKDEV(button_major, button_minor);
device_destroy(p_button_class, devno);
class_destroy(p_button_class);
cdev_del(&button_cdev);
unregister_chrdev_region(devno, 1);
}
module_init(button_init);
module_exit(button_exit);
MODULE_AUTHOR("Jimmy");
MODULE_DESCRIPTION("button Driver");
MODULE_LICENSE("GPL");
驱动Makefile文件
ifneq ($(KERNELRELEASE),)
obj-m := button.o
else
KERNELDIR ?= /ljm/git_imx6/linux-fsl/src/linux-3-14-28-r0
TARGET_CROSS = arm-none-linux-gnueabi-
PWD := $(shell pwd)
default:
$(MAKE) ARCH=arm CROSS_COMPILE=$(TARGET_CROSS) -C $(KERNELDIR) M=$(PWD) modules
endif
install:
$(MAKE) ARCH=arm CROSS_COMPILE=$(TARGET_CROSS) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.symvers *.order
应用程序main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/ioctl.h>
#define DEV_BUTTON "/dev/poll_button"
int main(void)
{
int dev_fd;
int ret;
char read_buf[20] = {-1};
struct timeval rto;
fd_set read_fds;
rto.tv_sec = 10;
rto.tv_usec = 0;
dev_fd = open(DEV_BUTTON, O_RDWR /*| O_NONBLOCK*/);
if ( dev_fd == -1 ) {
printf("open %s failed, ret = %d\n", DEV_BUTTON, dev_fd);
return -1;
}
while(1)
{
rto.tv_sec =10;
rto.tv_usec = 0;
FD_ZERO(&read_fds);
FD_SET(dev_fd, &read_fds);
ret = select(dev_fd+1, &read_fds, NULL, NULL, &rto);
if(ret == -1) {
printf("error\n");
continue;
} else if(ret == 0) {
printf("timeout\n");
continue;
} else {
if(FD_ISSET(dev_fd, &read_fds)) {
read(dev_fd, read_buf, 1);
printf("button pressed, val = %d\n", read_buf[0]);
}
}
}
printf("clsoe %s\n", DEV_BUTTON);
close(dev_fd);
return 0;
}
应用程序Makefile
WORKDIR =
INCLUDES = -I.
LIBS =
LINKS = -lpthread
CC = arm-none-linux-gnueabi-gcc
TARGET = main
src=$(wildcard *.c ./callback/*.c)
C_OBJS=$(patsubst %.c, %.o,$(src))
#C_OBJS=$(dir:%.c=%.o)
compile:$(TARGET)
$(C_OBJS):%.o:%.c
$(CC) $(CFLAGS) $(INCLUDES) -o $*.o -c $*.c
$(TARGET):$(C_OBJS)
$(CC) -o $(TARGET) $^ $(LIBS) $(LINKS)
@echo
@echo Project has been successfully compiled.
@echo
install: $(TARGET)
cp $(TARGET) $(INSTALL_PATH)
uninstall:
rm -f $(INSTALL_PATH)/$(TARGET)
rebuild: clean compile
clean:
rm -rf *.o $(TARGET) *.log *~
三、select的整体流程
应用层的select函数会调用到内核函数do_select,do_select调用驱动的poll函数,若poll函数返回的掩码不可读写,那么do_select进入睡眠阻塞。要从睡眠中醒来并且跳出,有两种情况:a、超时跳出;b、驱动中唤醒等待队列,这时do_select再次调用poll函数,如果poll函数返回的掩码可读写,那么就跳出阻塞,否则继续睡眠。注意:上述是在select函数设成阻塞的情况,select函数可以设置成非阻塞的(将select函数的timeout参数设置成0)。
四、select机制内核代码走读
调用顺序如下select() -> core_sys_select() -> do_select() -> fop->poll()
1、select函数解析
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
fd_set __user *, exp, struct timeval __user *, tvp)
{
struct timespec end_time, *to = NULL;
struct timeval tv;
int ret;
if (tvp) {// 如果超时值非NULL
if (copy_from_user(&tv, tvp, sizeof(tv))) // 从用户空间取数据到内核空间
return -EFAULT;
to = &end_time;
// 得到timespec格式的未来超时时间
if (poll_select_set_timeout(to,
tv.tv_sec + (tv.tv_usec / USEC_PER_SEC),
(tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC))
return -EINVAL;
}
ret = core_sys_select(n, inp, outp, exp, to); // 关键函数
ret = poll_select_copy_remaining(&end_time, tvp, 1, ret);
/*如果有超时值, 并拷贝离超时时刻还剩的时间到用户空间的timeval中*/
return ret; // 返回就绪的文件描述符的个数
}
2、core_sys_select函数解析
int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,
fd_set __user *exp, struct timespec *end_time)
{
fd_set_bits fds;
/**
typedef struct {
unsigned long *in, *out, *ex;
unsigned long *res_in, *res_out, *res_ex;
} fd_set_bits;
这个结构体中定义的全是指针,这些指针都是用来指向描述符集合的。
**/
void *bits;
int ret, max_fds;
unsigned int size;
struct fdtable *fdt;
/* Allocate small arguments on the stack to save memory and be faster */
long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];
// 256/32 = 8, stack中分配的空间
/**
@ include/linux/poll.h
#define FRONTEND_STACK_ALLOC 256
#define SELECT_STACK_ALLOC FRONTEND_STACK_ALLOC
**/
ret = -EINVAL;
if (n < 0)
goto out_nofds;
/* max_fds can increase, so grab it once to avoid race */
rcu_read_lock();
fdt = files_fdtable(current->files); // RCU ref, 获取当前进程的文件描述符表
max_fds = fdt->max_fds;
rcu_read_unlock();
if (n > max_fds) // 如果传入的n大于当前进程最大的文件描述符,给予修正
n = max_fds;
/*
* We need 6 bitmaps (in/out/ex for both incoming and outgoing),
* since we used fdset we need to allocate memory in units of
* long-words.
*/
size = FDS_BYTES(n);
// 以一个文件描述符占一bit来计算,传递进来的这些fd_set需要用掉多少个字
bits = stack_fds;
if (size > sizeof(stack_fds) / 6) {
// 除6,为什么?因为每个文件描述符需要6个bitmaps
/* Not enough space in on-stack array; must use kmalloc */
ret = -ENOMEM;
bits = kmalloc(6 * size, GFP_KERNEL); // stack中分配的太小,直接kmalloc
if (!bits)
goto out_nofds;
}
// 这里就可以明显看出struct fd_set_bits结构体的用处了。
fds.in = bits;
fds.out = bits + size;
fds.ex = bits + 2*size;
fds.res_in = bits + 3*size;
fds.res_out = bits + 4*size;
fds.res_ex = bits + 5*size;
// get_fd_set仅仅调用copy_from_user从用户空间拷贝了fd_set
if ((ret = get_fd_set(n, inp, fds.in)) ||
(ret = get_fd_set(n, outp, fds.out)) ||
(ret = get_fd_set(n, exp, fds.ex)))
goto out;
zero_fd_set(n, fds.res_in); // 对这些存放返回状态的字段清0
zero_fd_set(n, fds.res_out);
zero_fd_set(n, fds.res_ex);
ret = do_select(n, &fds, end_time); // 关键函数,完成主要的工作
if (ret < 0) // 有错误
goto out;
if (!ret) { // 超时返回,无设备就绪
ret = -ERESTARTNOHAND;
if (signal_pending(current))
goto out;
ret = 0;
}
// 把结果集,拷贝回用户空间
if (set_fd_set(n, inp, fds.res_in) ||
set_fd_set(n, outp, fds.res_out) ||
set_fd_set(n, exp, fds.res_ex))
ret = -EFAULT;
out:
if (bits != stack_fds)
kfree(bits); // 如果有申请空间,那么释放fds对应的空间
out_nofds:
return ret; // 返回就绪的文件描述符的个数
}
3、do_select函数解析
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
ktime_t expire, *to = NULL;
struct poll_wqueues table;
poll_table *wait;
int retval, i, timed_out = 0;
unsigned long slack = 0;
rcu_read_lock();
// 根据已经设置好的fd位图检查用户打开的fd, 要求对应fd必须打开, 并且返回
// 最大的fd。
retval = max_select_fd(n, fds);
rcu_read_unlock();
if (retval < 0)
return retval;
n = retval;
// 一些重要的初始化:
// poll_wqueues.poll_table.qproc函数指针初始化,该函数是驱动程序中poll函数实
// 现中必须要调用的poll_wait()中使用的函数。
poll_initwait(&table);
wait = &table.pt;
if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
wait = NULL;
timed_out = 1; // 如果系统调用带进来的超时时间为0,那么设置
// timed_out = 1,表示不阻塞,直接返回。
}
if (end_time && !timed_out)
slack = estimate_accuracy(end_time); // 超时时间转换
retval = 0;
for (;;) {
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
inp = fds->in; outp = fds->out; exp = fds->ex;
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
// 所有n个fd的循环
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
unsigned long in, out, ex, all_bits, bit = 1, mask, j;
unsigned long res_in = 0, res_out = 0, res_ex = 0;
const struct file_operations *f_op = NULL;
struct file *file = NULL;
// 先取出当前循环周期中的32个文件描述符对应的bitmaps
in = *inp++; out = *outp++; ex = *exp++;
all_bits = in | out | ex; // 组合一下,有的fd可能只监测读,或者写,或者e rr,或者同时都监测
if (all_bits == 0) { // 这32个描述符没有任何状态被监测,就跳入下一个32个fd的循环中
i += __NFDBITS; //每32个文件描述符一个循环,正好一个long型数
continue;
}
// 本次32个fd的循环中有需要监测的状态存在
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {// 初始bit = 1
int fput_needed;
if (i >= n) // i用来检测是否超出了最大待监测的fd
break;
if (!(bit & all_bits))
continue; // bit每次循环后左移一位的作用在这里,用来跳过没有状态监测的fd
file = fget_light(i, &fput_needed); // 得到file结构指针,并增加引用计数字段f_count
if (file) { // 如果file存在
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
if (f_op && f_op->poll) {
wait_key_set(wait, in, out, bit);// 设置当前fd待监测的事件掩码
mask = (*f_op->poll)(file, wait);
/*
调用驱动程序中的poll函数,以evdev驱动中的
evdev_poll()为例该函数会调用函数poll_wait(file, &evdev->wait, wait),
继续调用__pollwait()回调来分配一个poll_table_entry结构体,该结构体有一个内嵌的等待队列项,
设置好wake时调用的回调函数后将其添加到驱动程序中的等待队列头中。
*/
}
fput_light(file, fput_needed);
// 释放file结构指针,实际就是减小他的一个引用计数字段f_count。
// mask是每一个fop->poll()程序返回的设备状态掩码。
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit; // fd对应的设备可读
retval++;
wait = NULL; // 后续有用,避免重复执行__pollwait()
}
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit; // fd对应的设备可写
retval++;
wait = NULL;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit;
retval++;
wait = NULL;
}
}
}
// 根据poll的结果写回到输出位图里,返回给上级函数
if (res_in)
*rinp = res_in;
if (res_out)
*routp = res_out;
if (res_ex)
*rexp = res_ex;
/*
这里的目的纯粹是为了增加一个抢占点。
在支持抢占式调度的内核中(定义了CONFIG_PREEMPT),
cond_resched是空操作。
*/
cond_resched();
}
wait = NULL; // 后续有用,避免重复执行__pollwait()
if (retval || timed_out || signal_pending(current))
break;
if (table.error) {
retval = table.error;
break;
}
/*跳出这个大循环的条件有: 有设备就绪或有异常(retval!=0), 超时(timed_out
= 1), 或者有中止信号出现*/
/*
* If this is the first loop and we have a timeout
* given, then we convert to ktime_t and set the to
* pointer to the expiry value.
*/
if (end_time && !to) {
expire = timespec_to_ktime(*end_time);
to = &expire;
}
// 第一次循环中,当前用户进程从这里进入休眠,
// 上面传下来的超时时间只是为了用在睡眠超时这里而已
// 超时,poll_schedule_timeout()返回0;被唤醒时返回-EINTR
if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
to, slack))
timed_out = 1; /* 超时后,将其设置成1,方便后面退出循环返回到上层 */
}
// 清理各个驱动程序的等待队列头,同时释放掉所有空出来的page页(poll_table_entry)
poll_freewait(&table);
return retval; // 返回就绪的文件描述符的个数
}