高级IO
1.关于IO
IO的基本类型:
I代表输入(Input):
- 从外部设备或来源(如键盘、鼠标、文件、网络)读取数据到计算机中。
- 示例:用户键入的文本、从文件读取的数据、从网络接收到的数据包。
O代表输出(Output):
- 将计算机处理后的数据发送到外部设备或目的地(如显示器、打印机、文件、网络)。
- 示例:屏幕上显示的文本、写入文件的数据、发送到网络的消息。
2.IO模型
①.阻塞IO
在进行IO操作时,当前进程或者线程会被阻塞,直到IO操作完成。
例如scanf,在你没有输入任何内容时,他会卡在那里,直到等到你输入内容回车后,菜继续执行。那么在没有输入任何内容时,进程就进入到了阻塞态,等待你输入内容,然后进入到就绪队列,然后被CPU调度变为运行态,最终执行完成。
②.非阻塞IO
- 轮询:比如你又一个快递,你会去查看,但是查看之后你不会一直去等待这个快递在这个过程中什么事你都不去干。正常来说,你会看一下快递到了吗,然后就去干别的事,然后过一段时间又看一下到了吗,直到你的快递到达。这个过程就叫做轮询
- 信号机制:等待一个信号的发生,然后做出对应的操作
- 通信机制:计算机网络
- 非阻塞IO适用于IO多路复用
③.缓存IO
- 缓存:当你寄快递,快递公司不可能只为了
一个快递发一个车,他会等到一定数量才发车。这就叫做缓存。#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 行缓冲机制:C语言中的3个标准流
④.直接IO
- 直接写入到硬盘,没有任何缓存。
⑤.同步IO
- 当你通过IO进行写入时,他会写入到缓存,然后当你保存时写入到磁盘上,最后完全写入到磁盘后才进行返回,保存成功。
3.IO多路复用
为什么要引入IO多路复用?
为了是一个程序能够同时监听多个文件描述符(文件、套接字等)以等待事件(如数据到达、IO写入等等)。这样通过这样就可以更方便的去管理阻塞的问题。
IO多路复用相应函数:
select
函数原型:
#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数:
- int nfds:需要监视的文件描述符数目,是所有文件描述符中最大值加1,例如现在我需要监听0,1,2,也就是我程序对应的三个标准流输入,输出,错误输出。那么nfds的值就应该为3
- fd_set *readfds:监视是否可读的文件描述符集合
- fd_set *writefds:监视是否可写的文件描述符集合
- fd_set *exceptfds:监视是否有异常条件的文件描述符集合
上面3个参数,都分别可以看为文件fd的集合,也就是每一个对应的就是一个fd池,可能对应的每一个fd池中会有多个fd的文件描述符。select会分别去监视每个对应的池。
一些对于fd_set的宏操作:
FD_ZERO(fd_set *set)
:将fd_set
清空。FD_SET(int fd, fd_set *set)
:将文件描述符fd
加入fd_set
。FD_CLR(int fd, fd_set *set)
:从fd_set
中移除文件描述符fd
。FD_ISSET(int fd, fd_set *set)
:检查fd
是否在fd_set
中。
- struct timeval *timeout:等待的超时时间。如果设置为NULL,那么select将一直阻塞,f直到有反馈。
struct timeval {long tv_sec; /* seconds 秒*/long tv_usec; /* microseconds 微秒*/ };
返回值
返回-1:表示发生错误,并设置“errno"
返回0:表示再指定的事件超时,并没有文件描述符就绪
返回正数:表示有一个或者多个文件描述符就绪
例子:
#include <stdio.h> #include <sys/select.h> #include <stdlib.h> #include <unistd.h>int main() {fd_set refd;struct timeval timeout;int fd = 0;//标准输入流FD_ZERO(&refd);//初始化清空一下fd_setFD_SET(fd, &refd);//将标准输入流放入refd这个fd_set中//设置超时时间timeout.tv_sec = 5;timeout.tv_usec = 0;int ret = select(fd + 1, &refd, NULL, NULL, &timeout);//说明没有文件if (ret == 0) {fprintf(stderr, "Timeout Nothing");exit(1);}//发生错误if (ret < 0) {perror("select error");exit(2);}//开始对标准输入流读取char buff[100] = {0};ssize_t byt_read = read(fd, buff, sizeof(buff) - 1);if (byt_read < 0) {perror("read error");exit(1);}printf("sucessful read : ");printf("%s", buff);return 0; }
演示结果:
优缺点
优点:简单易用,跨平台,适用于少量文件描述符,文件描述符上限为1024
缺点:再处理大量文件描述符时性能会下降。
poll
函数原型
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
- struct pollfd &fds:指向pollfd结构体数组的指针,这个结构体中包含了需要监听的文件描述符,以及监听事件和实际发生的事件。
- nfds_t nfds:fds数组的大小
- int timeout:等待的超时事件,为-1就一直阻塞等待,为0就立即返回;为正数那么就是毫秒数内阻塞
struct pollfd {int fd; // 文件描述符short events; // 监视的事件short revents; // 实际发生的事件 };
- int fd:监听的文件描述符
- events:指定要监视的实际事件:
POLLIN
:有数据可读。POLLRDNORM
:普通数据可读。POLLRDBAND
:优先数据可读。POLLPRI
:有紧急数据可读。POLLOUT
:可写数据。POLLWRNORM
:普通数据可写。POLLWRBAND
:优先数据可写。POLLERR
:发生错误。POLLHUP
:挂起。POLLNVAL
:无效请求。3.revents:由内核设置,指实际发生的事
返回值
正数:就绪文件数量
0:表示超时了没有任何文件就绪
-1:发生错误,设置errno
例子:
#include <stdio.h> #include <poll.h> #include <stdlib.h> #include <unistd.h>int main(){struct pollfd fds[1];int fd = 0;//还是监听标准输入流fds[0].fd = fd;fds[0].events = POLLIN; //监视fds集合,集合中只有一个文件描述符,等但时间为5000msint ret = poll(fds, 1, 5000);//发生错误 if (ret == -1) {perror("poll error");}if (ret == 0) {fprintf(stderr, "poll Nothing\n");exit(1);}//与上它为真说明发生的事件就是这个if (fds[0].revents & POLLIN) {char buff[100] = {0};ssize_t byt_read = read(fd, buff, sizeof(buff) - 1);if (byt_read < 0) {perror("read error");exit(1);}printf("poll sucess : %s", buff);}return 0; }
运行结果:
优缺点:
优点:
- 对于select没有文件描述符数量的限制,适合处理大量文件描述符
- 更灵活,提供了许多处理事件的接口,允许监视更过种类的事件
缺点:
- 在处理大量文件描述符时,poll需要扫描整个文件描述符数组,那么性能就会下降
- 对比select,poll的使用更复杂‘
epoll函数家族
epoll_createl:创建一个epoll
函数原型
#include <sys/epoll.h>int epoll_create1(int flags);
参数
flags
:创建标志,可以为0
或EPOLL_CLOEXEC
,后者在创建epoll
文件描述符时设置FD_CLOEXEC
标志。返回值
- 成功时,返回
epoll
实例的文件描述符。- 失败时,返回
-1
并设置errno
。eooll_ctl:控制epoll实例上的事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数
epfd
:由epoll_create1
返回的epoll
实例文件描述符。op
:要执行的操作,可以是以下值之一:
EPOLL_CTL_ADD
:添加事件。EPOLL_CTL_MOD
:修改事件。EPOLL_CTL_DEL
:删除事件。fd
:需要操作的目标文件描述符。event
:指向epoll_event
结构体的指针,指定感兴趣的事件和关联的数据。epoll_event
struct epoll_event {uint32_t events; // 监视的事件epoll_data_t data; // 用户数据 };
events
:感兴趣的事件,可以是以下值的组合:
EPOLLIN
:有数据可读。EPOLLOUT
:可写数据。EPOLLRDHUP
:对方关闭连接或半关闭连接。EPOLLPRI
:有紧急数据可读。EPOLLERR
:发生错误。EPOLLHUP
:挂起。EPOLLET
:边沿触发。EPOLLONESHOT
:一次性事件。data
:用户数据,可用于存储文件描述符或其他用户定义的数据。epoll_data_t
typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64; } epoll_data_t;
epoll_wait:等待事件的发生
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数
epfd
:由epoll_create1
返回的epoll
实例文件描述符。events
:指向epoll_event
结构体数组的指针,用于存储发生的事件。maxevents
:数组的大小,即一次最多返回的事件数。timeout
:等待的超时时间(以毫秒为单位)。如果为-1
,则无限期阻塞;如果为0
,则立即返回。返回值
- 返回正值:表示已就绪的文件描述符数量。
- 返回
0
:表示在指定的超时时间内没有文件描述符就绪。- 返回
-1
:表示发生错误,并设置errno
。例子
#include <stdio.h> #include <sys/epoll.h> #include <unistd.h> #include <fcntl.h> #include <stdlib.h>int main() {int epoll_fd = epoll_create1(0);if (epoll_fd < 0) {perror("epoll_create1 error");exit(1);}struct epoll_event event;event.events = EPOLLIN;//监听可读事件event.data.fd = 0;//文件描述符设置为0,标准输入if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event) < 0) {perror("epoll_ctl error");close(epoll_fd);exit(1);}struct epoll_event events[1];int ret = epoll_wait(epoll_fd, events, 1, 5000);if (ret < 0) {perror("epoll_wait error");exit(1);}if (ret == 0) {fprintf(stderr, "Timeout epoll_wait Nothing\n");exit(1);}if (events[0].events & EPOLLIN) {char buff[100] = {0};ssize_t b_read = read(0, buff, sizeof(buff) - 1);if (b_read < 0) {perror("b_read error");exit(1);}printf("epoll sucess : %s", buff);}close(epoll_fd);return 0; }
优缺点:
优点:
- 高效,性能不会随着文件描述符增多而增多
- 边沿触发:epoll支持边沿触发模式,可以减少系统调用次数,提高性能,也就是减少变态次数
- 支持一次性事件:使用
EPOLLONESHOT
标志设置一次性事件,并在事件处理完毕后再次将文件描述符添加回epoll
实例缺点
- 只能再linux上使用,是linux特有的系统调用,无法跨平台
- 对于poll和select调用更复杂