C语言-IO
一,阻塞IO与非阻塞IO
简介:
在 C 语言中,阻塞 I/O 和非阻塞 I/O 是两种不同的输入 / 输出操作方式,它们在程序的行为和性能方面有很大的区别。
一、阻塞 I/O
-
概念:
- 当一个进程进行阻塞 I/O 操作时,如果数据尚未准备好或者输出缓冲区已满,进程会被阻塞,暂停执行,直到 I/O 操作完成。
- 例如,当使用
read
函数从一个文件描述符读取数据时,如果没有数据可读,进程会一直等待,直到有数据到达或者文件描述符被关闭。
-
特点:
- 简单直观:编程模型相对简单,容易理解和实现。
- 同步操作:进程在进行 I/O 操作时会等待操作完成,因此是一种同步的操作方式。
- 低并发性:由于进程在进行 I/O 操作时会被阻塞,因此在一个单线程程序中,只能同时进行一个 I/O 操作,降低了系统的并发性。
-
示例代码:
#include <stdio.h>
#include <unistd.h>int main() {int fd = open("test.txt", O_RDONLY);if (fd == -1) {perror("open");return 1;}char buffer[1024];ssize_t bytesRead = read(fd, buffer, sizeof(buffer));if (bytesRead == -1) {perror("read");return 1;}close(fd);return 0;
}
在这个例子中,如果test.txt
文件中没有数据可读,read
函数会阻塞进程,直到有数据可读或者文件描述符被关闭。
二、非阻塞 I/O
-
概念:
- 非阻塞 I/O 允许进程在进行 I/O 操作时不会被阻塞。如果数据尚未准备好或者输出缓冲区已满,I/O 函数会立即返回一个错误码,表示操作无法立即完成。
- 进程可以通过轮询的方式不断检查 I/O 状态,直到数据准备好或者操作可以完成。
-
特点:
- 高并发性:进程在进行 I/O 操作时不会被阻塞,因此可以同时进行多个 I/O 操作,提高了系统的并发性。
- 复杂编程模型:需要进程不断地进行轮询,增加了编程的复杂性。
- 可能浪费 CPU 时间:如果数据一直不可用,进程会不断地进行轮询,浪费 CPU 时间。
-
示例代码:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>int main() {int fd = open("test.txt", O_RDONLY | O_NONBLOCK);if (fd == -1) {perror("open");return 1;}char buffer[1024];ssize_t bytesRead = 0;while ((bytesRead = read(fd, buffer, sizeof(buffer))) == -1 && errno == EAGAIN) {// 文件不可读,继续轮询}if (bytesRead > 0) {// 处理读取到的数据} else {if (bytesRead == 0) {// 到达文件末尾} else {perror("read");}}close(fd);return 0;
}
在这个例子中,使用O_NONBLOCK
标志打开文件,使文件描述符处于非阻塞模式。如果文件中没有数据可读,read
函数会立即返回-1
,并且errno
被设置为EAGAIN
,表示文件不可读。进程可以通过不断地轮询来检查文件是否可读,直到有数据可读或者文件描述符被关闭。
三、阻塞 I/O 和非阻塞 I/O 的选择
-
应用场景:
- 阻塞 I/O 适用于简单的程序,其中 I/O 操作相对较少,并且不需要高并发性。例如,一个命令行工具,只需要从标准输入读取数据并进行处理,然后输出结果。
- 非阻塞 I/O 适用于需要高并发性的程序,其中多个 I/O 操作可以同时进行。例如,一个网络服务器,需要同时处理多个客户端的连接请求,并且不能因为一个客户端的 I/O 操作而阻塞其他客户端的请求处理。
-
性能考虑:
- 阻塞 I/O 在 I/O 操作完成之前会阻塞进程,因此可能会导致程序的响应时间较长。但是,由于进程在进行 I/O 操作时不会消耗 CPU 时间,因此在 I/O 操作频繁的情况下,可能会比非阻塞 I/O 更高效。
- 非阻塞 I/O 需要进程不断地进行轮询,因此会消耗一定的 CPU 时间。但是,由于进程在进行 I/O 操作时不会被阻塞,因此可以同时进行多个 I/O 操作,提高了系统的并发性。在 I/O 操作不频繁的情况下,非阻塞 I/O 可能会比阻塞 I/O 更高效。
总之,阻塞 I/O 和非阻塞 I/O 是两种不同的输入 / 输出操作方式,它们在程序的行为和性能方面有很大的区别。在选择使用哪种方式时,需要根据具体的应用场景和性能要求进行考虑。
二,多路复用IO-select
在 C 语言中,多路复用 I/O(I/O multiplexing)是一种可以同时监视多个文件描述符(file descriptor)的输入 / 输出状态的技术。其中,select
函数是一种常用的实现多路复用 I/O 的方法。
简介

一、select
函数的概念和用法
- 函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
-
参数说明:
nfds
:要监视的文件描述符的范围,通常设置为最高文件描述符值加 1。readfds
、writefds
、exceptfds
:分别是指向要监视的可读、可写和异常文件描述符集合的指针。可以为NULL
,表示不监视相应类型的文件描述符。timeout
:指定等待的时间限制。可以为NULL
,表示无限期等待;或者设置一个特定的时间值,表示等待的最长时间。
-
返回值:
- 返回值表示就绪的文件描述符的数量。如果在等待时间内没有任何文件描述符就绪,
select
返回 0。如果发生错误,返回 -1,并设置errno
。
- 返回值表示就绪的文件描述符的数量。如果在等待时间内没有任何文件描述符就绪,
- 单进程可以处理,但是需要不断检测客户端是否发出 IO 请求,需要不断占用 cpu ,消耗 cpu 资源
二、使用步骤
- 初始化文件描述符集合:
- 使用
fd_set
类型的变量来表示文件描述符集合。可以使用FD_ZERO
宏初始化一个空集合,使用FD_SET
宏将特定的文件描述符添加到集合中。
- 使用
fd_set readfds;FD_ZERO(&readfds);FD_SET(socket_fd, &readfds);
- 调用
select
函数:- 将初始化后的文件描述符集合作为参数传递给
select
函数,并设置适当的超时时间。
- 将初始化后的文件描述符集合作为参数传递给
struct timeval timeout;timeout.tv_sec = 5;timeout.tv_usec = 0;int ready = select(nfds, &readfds, NULL, NULL, &timeout);
- 检查就绪的文件描述符:
- 根据
select
的返回值,检查哪些文件描述符就绪。可以使用FD_ISSET
宏来测试特定的文件描述符是否在就绪集合中。
- 根据
if (ready > 0) {if (FD_ISSET(socket_fd, &readfds)) {// 处理可读的文件描述符}} else if (ready == 0) {// 超时} else {// 错误处理}
三、示例代码
以下是一个使用select
函数实现简单服务器的示例,该服务器可以同时处理多个客户端连接:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/time.h>#define PORT 8888
#define MAX_CLIENTS 10void handleClient(int client_fd) {char buffer[1024];ssize_t bytesRead;while ((bytesRead = read(client_fd, buffer, sizeof(buffer))) > 0) {// 处理客户端请求write(client_fd, buffer, bytesRead);}close(client_fd);
}int main() {int server_fd, client_fd;struct sockaddr_in server_addr, client_addr;socklen_t client_addr_len = sizeof(client_addr);fd_set readfds;int max_fd;int i;// 创建服务器套接字server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("socket");exit(EXIT_FAILURE);}// 设置服务器地址server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);// 绑定服务器套接字if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind");exit(EXIT_FAILURE);}// 监听连接请求if (listen(server_fd, MAX_CLIENTS) == -1) {perror("listen");exit(EXIT_FAILURE);}printf("Server listening on port %d...\n", PORT);// 初始化文件描述符集合FD_ZERO(&readfds);FD_SET(server_fd, &readfds);max_fd = server_fd;while (1) {fd_set tmpfds = readfds;int ready = select(max_fd + 1, &tmpfds, NULL, NULL, NULL);if (ready == -1) {perror("select");exit(EXIT_FAILURE);}if (FD_ISSET(server_fd, &tmpfds)) {// 有新的连接请求client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);if (client_fd == -1) {perror("accept");continue;}FD_SET(client_fd, &readfds);if (client_fd > max_fd) {max_fd = client_fd;}printf("New client connected.\n");} else {// 处理已连接的客户端for (i = 0; i <= max_fd; i++) {if (FD_ISSET(i, &tmpfds)) {if (i!= server_fd) {handleClient(i);FD_CLR(i, &readfds);if (i == max_fd) {while (FD_ISSET(max_fd, &readfds) == 0 && max_fd > server_fd) {max_fd--;}}}}}}}close(server_fd);return 0;
}
在这个例子中,服务器使用select
函数来监视服务器套接字和已连接的客户端套接字。当有新的连接请求时,服务器接受连接并将新的客户端套接字添加到文件描述符集合中。当有客户端发送数据时,服务器读取数据并将其回显给客户端。
四、注意事项
- 文件描述符限制:
select
函数的最大文件描述符数量通常受到系统限制。可以使用FD_SETSIZE
宏来查看系统支持的最大文件描述符数量。 - 性能问题:
select
函数在每次调用时都需要重新设置文件描述符集合,并且在返回时需要遍历所有的文件描述符来确定哪些是就绪的。这可能会导致性能问题,特别是在处理大量文件描述符时。 - 超时处理:可以使用
select
函数的timeout
参数来设置超时时间,以避免无限期地等待。如果超时时间到达,select
将返回 0,表示没有文件描述符就绪。
在 C 语言中,poll
是另一种实现多路复用 I/O 的方法。与select
相比,poll
在一些方面有改进。
三,多路复用IO-poll
简介:

一、poll
函数的概念和用法
- 函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
-
参数说明:
fds
:是一个pollfd
结构数组的指针,每个结构表示一个要监视的文件描述符及其事件。nfds
:是要监视的文件描述符数组的长度。timeout
:指定等待的时间限制,以毫秒为单位。可以为负值,表示无限期等待;为 0 表示立即返回;为正值表示等待指定的时间。
-
返回值:
- 返回值表示就绪的文件描述符的数量。如果在等待时间内没有任何文件描述符就绪,
poll
返回 0。如果发生错误,返回 -1,并设置errno
。
- 返回值表示就绪的文件描述符的数量。如果在等待时间内没有任何文件描述符就绪,
二、pollfd
结构
pollfd
结构通常定义如下:
struct pollfd {int fd; // 文件描述符short events; // 要监视的事件short revents; // 实际发生的事件
};
其中,events
成员用于指定要监视的事件类型,revents
成员在poll
返回时被设置为实际发生的事件类型。
常见的事件类型有:
POLLIN
:表示文件描述符可读。POLLOUT
:表示文件描述符可写。POLLPRI
:表示有紧急数据可读。POLLERR
:表示发生错误。POLLHUP
:表示挂起。
三、使用步骤
- 定义
pollfd
结构数组并初始化:
struct pollfd fds[10];fds[0].fd = socket_fd;fds[0].events = POLLIN;
- 调用
poll
函数:
int ready = poll(fds, 10, -1);
- 检查就绪的文件描述符:
if (ready > 0) {if (fds[i].revents & POLLIN) {// 处理可读的文件描述符}} else if (ready == 0) {// 超时} else {// 错误处理}
四、示例代码
以下是一个使用poll
实现简单服务器的示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>#define PORT 8888
#define MAX_CLIENTS 10void handleClient(int client_fd) {char buffer[1024];ssize_t bytesRead;while ((bytesRead = read(client_fd, buffer, sizeof(buffer))) > 0) {// 处理客户端请求write(client_fd, buffer, bytesRead);}close(client_fd);
}int main() {int server_fd, client_fd;struct sockaddr_in server_addr, client_addr;socklen_t client_addr_len = sizeof(client_addr);struct pollfd fds[MAX_CLIENTS + 1];int nfds = 1;// 创建服务器套接字server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("socket");exit(EXIT_FAILURE);}// 设置服务器地址server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);// 绑定服务器套接字if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind");exit(EXIT_FAILURE);}// 监听连接请求if (listen(server_fd, MAX_CLIENTS) == -1) {perror("listen");exit(EXIT_FAILURE);}printf("Server listening on port %d...\n", PORT);// 初始化文件描述符数组fds[0].fd = server_fd;fds[0].events = POLLIN;while (1) {int ready = poll(fds, nfds, -1);if (ready == -1) {perror("poll");exit(EXIT_FAILURE);}if (fds[0].revents & POLLIN) {// 有新的连接请求client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);if (client_fd == -1) {perror("accept");continue;}if (nfds == MAX_CLIENTS + 1) {fprintf(stderr, "Too many clients.\n");close(client_fd);} else {fds[nfds].fd = client_fd;fds[nfds].events = POLLIN;nfds++;printf("New client connected.\n");}} else {// 处理已连接的客户端for (int i = 1; i < nfds; i++) {if (fds[i].revents & POLLIN) {handleClient(fds[i].fd);fds[i].fd = -1;fds[i].events = 0;if (i == nfds - 1) {nfds--;}}}}}close(server_fd);return 0;
}
在这个例子中,服务器使用poll
来监视服务器套接字和已连接的客户端套接字。当有新的连接请求时,服务器接受连接并将新的客户端套接字添加到pollfd
结构数组中。当有客户端发送数据时,服务器读取数据并将其回显给客户端。
五、poll
与select
的比较
- 可监视的文件描述符数量:
poll
没有最大文件描述符数量的限制,而select
通常受到系统限制。 - 参数传递:
poll
使用pollfd
结构数组,不需要像select
那样每次重新设置文件描述符集合,减少了一些开销。 - 可移植性:
select
在不同的平台上更具可移植性,而poll
在一些特定的系统上可能表现更好。
总之,poll
是一种实现多路复用 I/O 的有效方法,在某些情况下比select
更具优势。但在实际应用中,还可以考虑使用更高效的多路复用技术,如epoll
。
四,多路复用IO-epoll
在 C 语言中,epoll
是一种高效的 I/O 多路复用机制,它克服了传统的select
和poll
函数在处理大量文件描述符时的性能瓶颈。
简介:


一、epoll
的概念和特点
-
epoll
的工作原理:epoll
通过在内核中维护一个事件表,将需要监视的文件描述符及其感兴趣的事件注册到这个事件表中。- 当文件描述符上有事件发生时,内核会将这些事件通知给应用程序,应用程序可以根据这些通知进行相应的 I/O 操作。
-
与
select
和poll
的比较:select
和poll
在每次调用时都需要遍历所有的文件描述符,检查它们是否有事件发生,这种方式在处理大量文件描述符时效率低下。epoll
只需要在文件描述符状态发生变化时才会通知应用程序,避免了不必要的遍历,因此在处理大量文件描述符时具有更高的性能。
-
epoll
的事件触发模式:epoll
支持两种事件触发模式:水平触发(Level Triggered,LT)和边缘触发(Edge Triggered,ET)。- 在水平触发模式下,只要文件描述符上有事件发生,
epoll
就会不断地通知应用程序,直到应用程序对该事件进行处理。 - 在边缘触发模式下,只有当文件描述符的状态从不可读 / 不可写变为可读 / 可写时,
epoll
才会通知应用程序。这种模式需要应用程序在一次通知中尽可能多地处理事件,以避免丢失事件。
二、使用epoll
的步骤
-
创建
epoll
实例:- 使用
epoll_create
函数创建一个epoll
实例,该函数返回一个文件描述符,用于后续的epoll
操作。 - 函数原型:
int epoll_create(int size);
- 参数
size
是一个提示性参数,表示epoll
实例可以处理的最大文件描述符数量。这个参数在现代 Linux 内核中已经被忽略,但仍然需要提供一个大于 0 的值。
- 使用
-
注册文件描述符和事件:
- 使用
epoll_ctl
函数将需要监视的文件描述符及其感兴趣的事件注册到epoll
实例中。 - 函数原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 参数说明:
epfd
是epoll
实例的文件描述符。op
是操作类型,可以是EPOLL_CTL_ADD
(添加文件描述符)、EPOLL_CTL_MOD
(修改文件描述符的事件)或EPOLL_CTL_DEL
(删除文件描述符)。fd
是要注册的文件描述符。event
是一个指向epoll_event
结构的指针,用于指定要监视的事件类型和相关的数据。
- 使用
-
等待事件发生:
- 使用
epoll_wait
函数等待epoll
实例上的事件发生。 - 函数原型:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 参数说明:
epfd
是epoll
实例的文件描述符。events
是一个指向epoll_event
结构数组的指针,用于存储发生的事件。maxevents
是events
数组的大小,表示最多可以返回的事件数量。timeout
是等待事件发生的超时时间,以毫秒为单位。可以设置为-1
表示无限期等待。
- 使用
-
处理事件:
- 当
epoll_wait
函数返回时,应用程序可以根据events
数组中的事件进行相应的 I/O 操作。 epoll_event
结构中的events
成员表示发生的事件类型,可以是EPOLLIN
(可读事件)、EPOLLOUT
(可写事件)等。
- 当
三、示例代码
以下是一个使用epoll
实现简单服务器的示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>#define PORT 8888
#define MAX_EVENTS 10void handleClient(int client_fd) {char buffer[1024];ssize_t bytesRead;while ((bytesRead = read(client_fd, buffer, sizeof(buffer))) > 0) {// 处理客户端请求write(client_fd, buffer, bytesRead);}close(client_fd);
}int main() {int server_fd, client_fd;struct sockaddr_in server_addr, client_addr;socklen_t client_addr_len = sizeof(client_addr);struct epoll_event event, events[MAX_EVENTS];int epoll_fd, nfds;// 创建服务器套接字server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("socket");exit(EXIT_FAILURE);}// 设置服务器地址server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);// 绑定服务器套接字if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind");exit(EXIT_FAILURE);}// 监听连接请求if (listen(server_fd, SOMAXCONN) == -1) {perror("listen");exit(EXIT_FAILURE);}printf("Server listening on port %d...\n", PORT);// 创建 epoll 实例epoll_fd = epoll_create1(0);if (epoll_fd == -1) {perror("epoll_create1");exit(EXIT_FAILURE);}// 将服务器套接字添加到 epoll 实例中,监视可读事件event.events = EPOLLIN;event.data.fd = server_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {perror("epoll_ctl");exit(EXIT_FAILURE);}while (1) {// 等待事件发生nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);if (nfds == -1) {perror("epoll_wait");exit(EXIT_FAILURE);}// 处理发生的事件for (int i = 0; i < nfds; i++) {if (events[i].data.fd == server_fd) {// 有新的连接请求client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);if (client_fd == -1) {perror("accept");continue;}// 将新的客户端套接字添加到 epoll 实例中,监视可读事件event.events = EPOLLIN;event.data.fd = client_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {perror("epoll_ctl");close(client_fd);}} else {// 处理客户端的请求handleClient(events[i].data.fd);// 从 epoll 实例中删除客户端套接字if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL) == -1) {perror("epoll_ctl");}}}}close(server_fd);close(epoll_fd);return 0;
}
在这个例子中,服务器使用epoll
来监视服务器套接字和已连接的客户端套接字。当有新的连接请求时,服务器接受连接并将新的客户端套接字添加到epoll
实例中。当有客户端发送数据时,服务器读取数据并将其回显给客户端。
四、注意事项
-
错误处理:
- 在使用
epoll
函数时,要注意检查返回值并进行适当的错误处理。 - 如果
epoll_create1
、epoll_ctl
或epoll_wait
函数返回错误,应该根据错误码进行相应的处理。
- 在使用
-
事件触发模式:
- 根据应用程序的需求选择合适的事件触发模式。水平触发模式相对简单,但可能会导致频繁的通知;边缘触发模式需要应用程序更加小心地处理事件,以避免丢失事件。
-
资源管理:
- 在使用完
epoll
实例后,应该及时关闭对应的文件描述符,以释放系统资源。
- 在使用完
总之,epoll
是一种高效的 I/O 多路复用机制,在处理大量文件描述符时具有明显的优势。通过正确地使用epoll
,可以提高应用程序的性能和并发性。