I/O复用使得程序能够同时监听多个文件描述符,这对提高程序的性能至关重要。
举个例子:
就好比你天天玩手机,你妈为了监控你,在你房间安装了一个监控,这个监控可以实时监控你的一举一动,并上传到你妈手机上,并提醒你妈,你在玩手机,快去揍他。你看着可不可怕,一看见你玩手机就揍你,没天理。
I/O复用就是这样 你妈把想监控的事件告诉监控,监控负责监控并通知你妈去 揍你(揍你就是对就绪事件做出的处理)。虽然这个例子不太好,但很形象。
系统函数调用
select系统调用
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数:
- nfds:参数类型为int,指被监听的所有文件描述符总数 。它通常被设置为select监听的所有文件描述符中的最大值+1,因为文件描述符是从0开始计数的。
- readfds,writefds,exceptfds:分别指向 可读,可写和异常等事件对应的文件描述符集合。通过这三个参数传入自己想要监控的文件描述符。select调用返回后,内核将修改他们来通知应用程序那些文件描述符已经就绪。
- timeout :timeout用来设置select函数的超时调用时间。
fd_set结构体中包含一个整型数组,该数组中每一个元素的每一个比特位标记一个文件描述符,但是fd_set容纳的文件描述符是有上限的 数量由FD_SETSIZE来指定。
返回值:
成功返回就绪(可读可写异常)文件描述符总数,在规定时间内没有就绪,就返回0:调用失败返回-1并设置errno;如果在等待时间内接收到信号,则立即返回-1,并设置errno为EINTR。
fd_set结构体类型
fd_set结构体仅包含一个整型数组,该数组中的每一个元素的每一个比特位标记一个文件描述符。里面通过一个宏(FD_SETSIZE),来限制select能同时处理文件的总量。
位操作:
由于位操作过于繁琐,于是就有一组宏来代替处理:
#include<sys/select.h>
FD_ZERO(fd_set* fdset);//清除fdset的所有位
FD_SET(int fd, fd_set *fdset);//设置fdset的位fd
FD_CLR(int fd,fd_set *fdset);//清除fdset的位fd
int FD_ISSET(int fd,fd_set *fdset);//测试fdset的位fd是否设置
timeout结构体类型
采用timeval结构体指针,是因为内核将修改它以告诉应用程序select等待了多久,但是select调用失败后,返回的这个值是不确定的。
下面就是select系统调用的简单使用
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
int main(void)
{
fd_set rfds;
struct timeval tv;
int retval;
//下面是监控输入文件描述符 fd = 0
/* Watch stdin (fd 0) to see when it has input. */
FD_ZERO(&rfds);//将rfds类型的变量的所有比特位置为0
FD_SET(0, &rfds);//设置rfds上面的比特位
/* Wait up to five seconds. *///设置超时时间
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv);//监控的文件描述符是0 填入是就是1
/* Don't rely on the value of tv now! */
if (retval == -1)
perror("select()");
else if (retval)//成功返回就绪文件描述符的总数
printf("Data is available now.\n");
/* FD_ISSET(0, &rfds) will be true. */
else
printf("No data within five seconds.\n");
exit(EXIT_SUCCESS);
}
上面代码是监控输入文件描述符,在五秒时间内,如果没有输入,就会返回0:有数据就会返回1,因为只监控了一个文件描述符,所以返回1。
你也可以进行循环监控。
poll系统调用
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
fds:fds参数就是一个pollfd结构体类型数组,后面会讲。
nfds:指定被监听事件集合大小(typedef unsigned long int nfds_t)就是长整型。
timeout:参数类型为int,单位为毫秒,指定poll的超时时间。当为-1时,poll将永远阻塞(相当于卡住了,没就绪就不返回),直到发生某个事件;当为0时,poll将立即返回(非阻塞(大白话:就是管你就不就绪,立刻返回))
返回值:
成功返回就绪(可读可写异常)文件描述符总数,在规定时间内没有就绪,就返回0:调用失败返回-1并设置errno;如果在等待时间内接收到信号,则立即返回-1,并设置errno为EINTR。
struct pollfd 结构体
- fd:你要监听的文件描述符。
- events:告诉poll你要监听fd上的那些事件。
- revent:由内核修改,通知应用程序fd上实际发生了那些事件。
poll事件监控类型
poll简单示例代码
#include <poll.h>
#include <fcntl.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0)
int main(int argc, char *argv[])
{
int nfds, num_open_fds;
struct pollfd *pfds;
if (argc < 2) {
fprintf(stderr, "Usage: %s file...\n", argv[0]);
exit(EXIT_FAILURE);
}
num_open_fds = nfds = argc - 1;
pfds = (struct pollfd*)malloc(nfds*sizeof(struct pollfd));
if (pfds == NULL)
errExit("malloc");
/* Open each file on command line, and add it 'pfds' array */
for (int j = 0; j < nfds; j++) {
pfds[j].fd = open(argv[j + 1], O_RDONLY);
if (pfds[j].fd == -1)
{
printf("111");
errExit("open");
}
printf("Opened \"%s\" on fd %d\n", argv[j + 1], pfds[j].fd);
pfds[j].events = POLLIN;//注册可读事件
}
/* Keep calling poll() as long as at least one file descriptor is
open */
while (num_open_fds > 0) {
int ready;
printf("About to poll()\n");
ready = poll(pfds, nfds, -1);
//ready = poll(pfds, nfds, -1);
if (ready == -1)
errExit("poll");
printf("Ready: %d\n", ready);
/* Deal with array returned by poll() */
for (int j = 0; j < nfds; j++) {
char buf[10];
if (pfds[j].revents != 0) {
printf(" fd=%d; events: %s%s%s\n", pfds[j].fd,
(pfds[j].revents & POLLIN) ? "POLLIN " : "",
(pfds[j].revents & POLLHUP) ? "POLLHUP " : "",
(pfds[j].revents & POLLERR) ? "POLLERR " : "");
if (pfds[j].revents & POLLIN) {
ssize_t s = read(pfds[j].fd, buf, sizeof(buf));
if (s == -1)
errExit("read");
printf(" read %zd bytes: %.*s\n",
s, (int) s, buf);
} else { /* POLLERR | POLLHUP */
printf(" closing fd %d\n", pfds[j].fd);
if (close(pfds[j].fd) == -1)
errExit("close");
num_open_fds--;
}
}
}
}
printf("All file descriptors closed; bye\n");
exit(EXIT_SUCCESS);
}
上述代码,我给的是一个文件test1,由于代码里面时死循环,并且文件一直都是可读的,所以就一直循环,最后ctrl+c进行终止。,但是你给个目录就只打印一次。
epoll系统调用
epoll是Linux特有的I/O复用函数。它在实现上和使用上与select,poll有很大的差异,首先,epoll使用一组函数来完成任务,而不是单个的。
epoll把用户关心的事件放到内核事件表中,而无需像select和poll那样每次调用都需要重复传入文件描述符或事件集。但epoll需要额外的文件描述符来标识唯一的内核事件表。
epoll_create
#include <sys/epoll.h>
int epoll_create(int size);
参数:
size:现在不起作用,提醒内核开多大空间,
返回值:
该函数返回的文件描述符将用作其他所有epoll系统调用的一个参数。
epoll_ ctl
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
- epfd:这个参数就是标识的内核事件表,epoll_create的返回值。
- op:指定操作类型
- EPOLL_CTL_ADD:添加fd上注册的事件
- EPOLL_CTL_MOD:修改fd上注册的事件
- EPOLL_CTL_DEL:删除fd上注册的事件
- fd:要操作的文件描述符
- event:指定事件,他是epoll_event结构体指针类型。
struct epoll_event
{
__uint_tevents;//epoll事件
epoll_data_t data;//用户数据
}
typedef struct union epoll_data
{
void*ptr;
int fd;
uint32 u32;
uint64 u64;
}epoll_data_t;
epoll支持的事件类型和poll事件类型基本相同,只需在poll事件类型的宏前面加"E".
epoll两个额外的事件类型EPOLLET和EPOLLONESHOT,他们对于epoll的高效运作非常重要。
后面在介绍。
返回值: 成返回0,失败返回-1并设置errno.
epoll_wait
epoll系列函数主要调用接口,它在一段超时时间内等待一组文件描述符。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
参数:
- epfd:就是epoll_create函数的返回值。
- timeout:与poll函数的timeout含义相同,单位毫秒。
- maxevents:最多监听多少个事件,他必须大于0。
- events:epoll_wait函数如果检测到事件,就将所有就绪的事件从内核时间表中复制到events指向的数组中。
返回值:
成功返回就绪的文件描述符的个数,失败返回-1并设置errno。
epoll简单的示例代码
#include <sys/epoll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int epoll_fd; // Epoll文件描述符
int sock; // 创建的套接字
struct epoll_event event;
// 创建套接字
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
perror("Failed to create socket");
return 1;
}
// 绑定和监听
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 端口例如8080
if (bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) {
perror("Failed to bind");
close(sock);
return 1;
}
listen(sock, 5);
// 初始化Epoll
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("Failed to create epoll instance");
close(sock);
return 1;
}
// 注册socket到Epoll
event.events = EPOLLIN | EPOLLET; // 监听读事件,并设置边缘触发
event.data.fd = sock;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &event) == -1) {
perror("Failed to add socket to epoll");
close(epoll_fd);
close(sock);
return 1;
}
printf("Listening on port %d...\n", ntohs(server_addr.sin_port));
while (true) {
// 等待Epoll有新的事件发生
size_t num_events = epoll_wait(epoll_fd, &event, 10, -1); // 检查最多10个事件
if (num_events > 0) {
for (int i = 0; i < num_events; ++i) {
if (event.events & EPOLLIN) { // 如果是读就处理
char buffer[1024];
ssize_t bytes_received = recv(event.data.fd, buffer, sizeof(buffer), 0);
if (bytes_received > 0) {
printf("Received data: %s\n", buffer);
} else if (bytes_received == 0) {
printf("Client disconnected\n");
} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
// Resource temporarily unavailable, do nothing
} else {
perror("Error receiving data");
}
}
}
}
}
close(epoll_fd);
close(sock);
return 0;
}
上面的代码中提到了边缘触发,这是epoll对文件描述符的操作有两种模式:
LT(Level Trigger电平触发):epoll相当于一个效率较高的poll。LT模式epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件,这样应用程序下一次调用epoll_wait时,epoll_wait还会再次触发该事件,直到这个事件被处理。
ET:当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。
epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以立即处理该事件,因为后续epoll_wait调用将不再向应用程序通知这一事件。
ET模式在很大程度上降低了同一个事件被重复触发的次数,效率要比LT模式高。
简记:
LT能拖则托(不立即处理)
ET积极向上(立即处理)
这里只是对epoll两种模式做一个简单介绍,后续会有专门的代码示例。