linux内核驱动休眠和唤醒机制 【sem_wait select 休眠等待实现机制】

阻塞与非阻塞的概念:

阻塞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; // 返回就绪的文件描述符的个数
}    

猜你喜欢

转载自blog.csdn.net/star871016/article/details/109671591