0%

底层 IO 模型

本文包含4种 IO模型 的介绍、实现原理分层解析、驱动编写思路和代码实现、以及应用层使用方法。

IO模型的分析

IO 模型是指应用程序在调用 read 函数的时候,如果数据没有准备好,此时进程会发生什么样的状态转换,以及什么时候会返回。在Linux中有五种IO模型,分别是阻塞IO模型,非阻塞IO模型,IO多路复用IO模型,信号驱动IO模型,异步IO模型。下面是对它的分析
以下内容仅为我的个人积累,详细内容请参考官方文档和相关书籍。

阻塞IO模型

在使用open打开设备文件的时候,如果没有指定 O_NONBLOCK,就说明使用的阻塞方式打开的文件。调用read函数想要从硬件中读取数据的时候,如果数据准备好了 read 就会立即返回,如果调用 read 的时候硬件的数据没有准备好进程休眠。当数据准备好的时候底层硬件会产生中断,内核的中断处理函数就会执行了,在中断处理函数中唤醒休眠的进程,将准备好的数据拷贝到用户空间即可。

阻塞IO模型的代码实现流程

应用层

1
2
fd = open("/dev/mycdev",O_RDWR);  //阻塞打开
read(fd,buf,sizeof(buf));

驱动层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* file_operations: */
driver_read(file,ubuf,size,offs)
{
if(file->f_flags & O_NONBLOCK){
//非阻塞
return -EINVAL;
}else{
//阻塞 (硬件数据是否准备好)
//如果数据没有准备好此时进程需要休眠
wait_event(wq_head, condition)
wait_event_interruptible(wq_head, condition)
}
//读取底层硬件的数据
//将读取到的数据拷贝到用户空间(copy_to_user)
}

/* 中断处理函数中:*/
condition = 1;
wake_up(&wq_head);
wake_up_interruptible(&wq_head)

IO多路复用IO模型

在同一个app应用程序中如果想要同时监听多个fd对应数据。就需要使用 select/poll/epoll 来完成监听。如果所有的文件描述符的数据都没有准备好,此时进程休眠。如果有一个或者多个硬件的数据准备好就会产生硬件中断,在处理函数中唤醒休眠的进程。此时 select/poll/epoll 就会返回,从就绪的表中找到准备好数据的文件描述符,然后调用 read 将数据读取到用户空间即可。

IO多路复用IO模型的代码实现流程

应用层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int fd1,fd2;
fd_set rfds; //定义读表
fd1 = open("/dev/mycdev0",O_RDWR);
fd2 = open("/dev/input/mouse0",O_RDWR);

while(1){
FD_ZERO(&rfds); //清空表
FD_SET(fd1,&rfds); //将fd1放到读表中
FD_SET(fd2,&rfds); //将fd2放到读表中
select(fd2+1,&rfds,NULL,NULL,NULL); //监听文件描述符

if(FD_ISSET(fd1,&rfds)){
read(fd1,buf1,sizeof(buf1));
printf("mycdev:%s\n",buf1);
}
if(FD_ISSET(fd2,&rfds)){
read(fd2,buf2,sizeof(buf2));
printf("mouse0:%s\n",buf2);
}
}

驱动层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* file_operations:(应用层select/poll/epoll对应驱动都是poll函数) */
__poll_t (*poll) (struct file *file, struct poll_table_struct *wait)
{
// 1.定义返回值变量
__poll_t mask=0;
// 2.调用poll_wait完成阻塞
poll_wait(file,&wq_head,wait);
// 3.如果数据准备好置位mask
if(condition){
mask |= EPOLLIN; //EPOLLIN 读 EPOLLOUT写
}
// 4.返回mask
return mask;
}

IO多路复用IO模型的实现原理

应用层

1
select(fd2 + 1, &rfds, NULL, NULL, NULL);

虚拟文件系统层

首先使用 vi -t sys_select 命令查看 select 函数的实现

  1. 对最大文件描述符的值作校验工作
  2. 在内核空间分配6张表的内存,其中前3张表用于保存用户传递到内核的文件描述符后三张表用于保存就绪的文件描述符(后三张表此时是空的)
  3. 遍历文件描述符
    mask = rfds-->fd-->fd_array[fd]-->file-->f_op-->poll(file,wait);
    判断mask返回的值,如果所有的文件描述对应驱动poll函数返回的值都是0,说明所有文件描述符的数据都没准备好,构造等待队列,进程休眠
  4. 如果一个或者多个文件描述符对应的数据准备好了,就会唤醒这个休眠的进程
  5. 再次遍历文件描述符
    mask = rfds-->fd-->fd_array[fd]-->file-->f_op-->poll(file,wait);
    找出mask不为0的文件描述符,将这个文件描述符放到就绪的文件描述符表中
  6. 将就绪的文件描述表拷贝到用户空间

总结

总的来说 select poll epoll 的实现原理都是一样的,只是在实现的时候有一些细节上的差别。

select (结构体)

  1. select监听的最大文件描述符限制1024
  2. select的内部实现又清空表的过程,需要反复构造表,从用户空间向内核空间拷贝表,效率低
  3. select从休眠状态被唤醒之后需要再次遍历文件描述符表,效率比较低

poll (链表)

  1. poll监听的文件描述符没有个数限制
  2. poll没有清空表的过程,效率高
  3. poll从休眠状态被唤醒之后需要再次遍历文件描述符表,效率比较低

epoll (红黑树+双链表)

  1. epoll监听的文件描述符没有个数限制
  2. epoll没有清空表的过程,效率高
  3. epoll监听的文件描述符就绪之后它能够直接拿到就绪的文件描述符,不需要遍历,效率高

epoll_ctl 支持管道,FIFO,套接字,POSIX消息队列,终端,设备等,但是就是不支持普通文件或目录的fd


异步通知IO模型

当底层硬件的数据准备好的时候会产生硬件中断,在驱动的中断处理函数中给对应的进程发送信号,当进程收到信号的时候去读取数据,当没有收到信号的时候进程可以执行任意操作。
信号和中断不同,中断是基于硬件实现的,而信号是基于软件实现的是中断的一种模拟,如果没有操作系统那么就没有信号。

异步通知IO模型的代码实现流程

应用层

首先在系统的信号中有一个 29) SIGIO 就是专门留给IO模型使用的,可以在终端通过 kill -l 命令查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 信号处理函数
void signal_handle(int signo)
{
//从底层读取数据
}
// 1.打开设备文件
fd = open("/dev/mycdev0",O_RDWR);

// 2.使用signal函数为信号绑定处理函数
// 要明白signal函数并不会调用file_operations中的任何函数,只是为信号绑定了一个处理函数
signal(SIGIO,signal_handle);

// 3.调用驱动的fasync函数,做初始化工作
unsigned int flags = fcntl(fd,F_GETFL);
fcntl(fd,F_SETFL,flags|FASYNC);

// 4.告诉内核接收信号的进程是当前进程
fcntl(fd,F_SETOWN,getpid());

虚拟文件系统层

首先可以使用 vi -t sys_fcntl 命令查看 fcntl 函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 可知首先执行的是
SYSCALL_DEFINE3(fcntl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
// 经过替换后可以得到
unsigned int sys_fcntl(unsigned int fd, unsigned int cmd, unsigned long arg)
--->err = do_fcntl(fd, cmd, arg, f.file);
// 然后知道会调用 do_fcntl 函数
static long do_fcntl(int fd, unsigned int cmd, unsigned long arg,struct file *filp)
--->switch (cmd) {
case F_GETFL:
err = filp->f_flags; //open函数的第二个参数,代表文件打开式
break;
case F_SETFL:
err = setfl(fd, filp, arg); //arg = filp->f_flags|FASYNC
break;
}

static int setfl(int fd, struct file * filp, unsigned long arg)
{
//arg = filp->f_flags|FASYNC
if (((arg ^ filp->f_flags) & FASYNC) && filp->f_op->fasync) {
//调用底层驱动的fasync函数执行
error = filp->f_op->fasync(fd, filp, (arg & FASYNC) != 0);
}
}

驱动层

1
2
3
4
5
6
7
8
9
10
11
/* file_operations: */
// 在内核中查看函数的实现,通过注释发现 fasnc_helper 函数是用来初始化异步通知的队列的,因为如果有多个进程都要异步通知,那么就需要一个队列来存储这些进程
int mycdev_fasync(int fd, struct file *file, int on)
{
//发信号前的初始化工作
//初始化一个异步通知的队列,你可以通过fapp成员拿到队列
return fasync_helper(fd, file, on, &fapp);
}

// 发送信号:
void kill_fasync(&fapp, SIGIO, POLL_IN); //POLL_IN 发送可读事件 POLL_OUT 发送可写事件