一,阻塞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 10
void 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 10
void 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 10
void 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
,可以提高应用程序的性能和并发性。