Linux下的IO
阻塞IO和非阻塞IO
- IO 本质是基于操作系统接口来控制底层的硬件之间数据传输,并且在操作系统中实现了多种不同的IO的方式(模型)
- IO 模型描述的是不同的 IO 方式,比较常用的几种
- 阻塞型 IO 模型9
- 非阻塞型 IO 模型
- 多路复用 IO 模型
- 阻塞型 IO
- 当进程发出 IO 请求后,阻塞进程(让进程进入睡眠状态),资源就绪后唤醒进程继续执行
- 一般默认的 IO 操作都是阻塞型 IO
- 特点
- 会一直等待,直到数据就绪
非阻塞型 IO
- 当进程发出 IO 请求后,无论资源是否就绪都会立即返回,相应的模型如下:
- 实现非阻塞 IO,需要设置 O_NONBLOCK 标志,设置有两种方式
- 可以通过调用 fcntl 函数来进行设置
- 通过 open 函数来进行设置,一般在打开文件时就需要设置
- fcntl 函数
- 函数头文件
- #include <unistd.h>
- #include <fcntl.h>
- 函数原型
- int fcntl(int fd, int cmd, ... /* arg */ );
- 函数功能
- 通过命令字(cmd)来设置文件描述符
- 函数参数
- fd : 文件描述符
- cmd : 控制命令字
- F_GETFD: 获取文件描述符标志
- F_SETFD: 设置文件描述符标志
- F_GETFL: 获取文件状态标志
- F_SETFL : 设置文件状态标志
- 函数头文件
- 注意
- fcntl 函数是一个可变参数列表函数
#include<stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main(void){
int flags;
char buffer[16] = {0};
flags = fcntl(0,F_GETFL);
flags |= O_NONBLOCK;
int ret = fcntl(0,F_SETFL,flags);
if(ret == -1){
perror("[ERRROR] fcntl");
exit(EXIT_FAILURE);
}
while(1){
fgets(buffer,sizeof(buffer),stdin);
printf("buffer: %s\n",buffer);
}
return 0;
}
IO多路复用
进程处理多路IO请求
- 在没有多路复用IO之前,对于多路IO请求,一般只有阻塞与非阻塞IO两种方式
- 阻塞IO
- 需要结合多进程与多线程,每个进程/线程处理一路I0
- 需要结合多进程与多线程,每个进程/线程处理一路I0
- 阻塞IO
- 缺点:
- 客户端越多,需要创建的进程/线程越多,相对占用内存资源较多
- 非阻塞IO
- 单进程可以处理,但是需要不断检测客户端是否发出IO请求,需要不断占用cpu,消耗 cpu 资源
多路复用 IO 简介
- 单进程可以处理,但是需要不断检测客户端是否发出IO请求,需要不断占用cpu,消耗 cpu 资源
- 本质上就是通过复用一个进程来处理多个 IO 请求,基本思想如下
- 由内核来监控多个文件描述符是否可以进行I/0操作,如果有就绪的文件描述符,将结果告知给用户进程,则用户进程在进行相应的 I/0 操作
- 由内核来监控多个文件描述符是否可以进行I/0操作,如果有就绪的文件描述符,将结果告知给用户进程,则用户进程在进行相应的 I/0 操作
三、多路复用方案
- 目前在 Linux 系统有三种 多路复用 I/0 的方案,具体如下:
- select 方案
- poll 方案
- epoll 方案
四、select 多路复用方案
- select 是多路复用I/0 的一种解决方案,具体的设计思想如下:
- 通过单进程创建一个文件描述符集合,将需要监控的文件描述符添加到这个集合中
- 由内核负责监控文件描述符是否可以进行读写,一旦可以读写,则通知相应的进程可以进行相应的IO操作
select的应用
- select 多路复用io在实现时主要是以调用 select 函数来实现
- select 函数的基本信息如下:
- 函数头文件
- #include <sys/select.h>
- 函数原型
- int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
- 函数功能
- 监控一组文件描述符,阻塞当前进程,由内核检测相应的文件描述符是否就绪,一旦有文件描述符就绪,将就绪的文件描述符拷贝给进程,唤醒进程处理
- 函数参数
- nfds: 最大文件描述符加1
- readfds : 读文件描述符集合的指针
- writefds : 写文件描述符集合的指针
- exceptfds: 其他文件描述符集合的指针
- timeout: 超时时间结构体变量的指针
- 函数返回值
- 成功 : 返回已经就绪的文件描述符的个数如果设置 timeout,超时就会返回 0
- 失败:-1,并设置 errno
- 函数头文件
- 操作文件描述符集合函数如下:
- void FD_CLR(int fd,fd set *set);
- 将 fd 从文件描述符集合中删除
- fd : 文件描述符
- set: 文件描述符集合的指针
FD_ISSET
- int FD_ISSET(int fd,fd set *set)
- 判断 fd 是否在文件描述符集合中
- fd : 文件描述符
- set: 文件描述符集合的指针
FD_SET
- void FD SET(int fd,fd set *set)
- 将文件描述符添加到文件描述符集合中
- fd: 文件描述符
- set : 文件描述符集合的指针
FD_ZERO
-
void FD_ZERO(fd_set *set)
-
将文件描述符集合清空
-
set: 文件描述符集合的指针
-
使用 select 函数监控标准输入,如果有输入,则打印相应的信息
#include<stdio.h>
#include <stdlib.h>
#include <sys/select.h>
int main(void){
int ret;
int maxfd = 0;
fd_set readfds, tmpfds;
struct timeval tv = {3,0},tmp_tv;
char buffer[64] = {0};
FD_ZERO(&readfds);
FD_SET(0,&readfds);
for(;;){
tmp_tv = tv;
tmpfds = readfds;
ret = select(maxfd + 1,&tmpfds,NULL,NULL,&tmp_tv);
if(ret == -1){
perror("[ERROR] select()");
exit(EXIT_FAILURE);
}
else if(ret == 0){
printf("Timeout\n");
}
else if(ret > 0){
if(FD_ISSET(0,&tmpfds)){
fgets(buffer,sizeof(buffer),stdin);
printf("buffer %s ",buffer);
}
}
}
return 0;
}
- 使用 select 监听有名管道,当有名管道有数据时,读取数据并打印
#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <stdlib.h>
#define FIFO_NAME "./fifo"
int main(){
int fd;
int ret;
char rbuffer[64];
fd = open(FIFO_NAME,O_RDWR);
if(fd == -1){
perror("[ERROR] open()");
exit(EXIT_FAILURE);
}
fd_set readfds,tmpfds;
int maxfd = fd;
struct timeval tv = {3,0},tmptv;
FD_ZERO(&readfds);
FD_SET(fd,&readfds);
//read(fd,rbuffer,sizeof(rbuffer));
//printf("recv: %s\n",rbuffer);
//exit(EXIT_SUCCESS);
for(;;){
tmptv = tv;
tmpfds = readfds;
ret = select(maxfd+1,&tmpfds,NULL,NULL,&tmptv);
if(ret == -1){
perror("[ERROR] select()");
exit(EXIT_FAILURE);
}
else if(ret == 0){
printf("Timeout\n");
}
else if(ret > 0){
if(FD_ISSET(fd,&tmpfds)){
read(fd, rbuffer,sizeof(rbuffer));
printf("recv: %s\n",rbuffer);
}
else{
printf("....");
}
}
}
return 0;
}
select原理
文件描述符集合
- 文件描述符集合在定义时的类型为 fd_set,在内核中的定义如下
- 数组的类型为 long int 类型 __fd_mask 为 typedef 产生的类型,在 64 位系统中 long int 的大小为 8 个字节
- typedef long int __fd_mask;
- NFDBITS 的大小为 64,FD_SETSIZE 为 1024,经过计算之后,大小为 16
- 文件描述符集合的数组最终在存储时,是使用了位图的方式来记录相应的文件描述符,具体原理如下:
- 数组中没有直接存储文件描述符,而是使用某一位来表示该文件描述符是否需要监控
- 需要监控的文件描述符需要转成数组的某一个元素的某一位,然后将对应的位设置为 1
- 比如当 fd = 60 的成员需要监控,则需要将数组的第0个成员的第[60] bit 设置为 1,
- 当 fd = 64 时,则需要将数组的第1个成员的第[0] bit 设置为 10
- 总结:
- 从上面的文件描述符集合内存管理可以分析出,select 最终只能存储 1024 个文件描述符
select底层分析
- select 基本的原理如下:
- 在 select()函数中一共需要使用三个文件描述符集合,分别是
- in: 读文件描述符集合,主要包含 需要进行读的文件描述符的集合,反映在底层实际可以从设备中读取数据
- out:写文件描述符集合,主要包含 需要进行写的文件描述符的集合,反映在底层实际可以将数据写入到设备
- exp: 其他文件描述符集合,主要包含其他类型的操作的文件描述符集合
- 一旦调用了 select() 函数,内核则做了如下事情:
- 从用户空间将集合的文件描述符拷贝到内核空间
- 循环遍历 fd set 中所有的文件描述符,来检测是否有文件描述符可进行I/0操作
- 如果有文件描述符可进行I/0操作,则设置返回的文件描述符集对应位为1(res_in,res_out,res_exp),表示可以进行I/0操作跳出循环,直接返回,最终会赋值给 in,out,exp 文件描述符集合
- 如果没有文件描述符可进行I/0操作,则继续循环检测,如果设置 timeout,则在超时后返回,此时 select()函数返回0
- select() 函数 减少了多进程/多线程的开销,但仍然有很多缺点:
- 每次调用 select0)函数都需要将 fd 集合拷贝到内核空间,这个开销在 fd 很多时越大
- 每次都需要遍历所有的文件描述符集合,这个开销在 fd 很多时越大
- 支持的文件描述符只有1024
select 系统调用分析
sys select 系统调用
- selelct 系统调用在内核中的实现如下
- 在 select 系统调用中对应的形式参数的值都是由 应用层 select 函数传递过来的
core_sys_select 函数
- sys_select 主要调用的核心函数为 core_sys_select,具体定义如下:
- 在 select 系统调用中对应的形式参数的值都是由 应用层 select 函数传递过来的