网络高级IO的五大模型主要包括:
阻塞IO(Blocking IO)
accept
和read
操作上,等待建立连接和读取数据。非阻塞IO(Non-blocking IO)
IO多路复用(IO Multiplexing)
信号驱动IO(Signal-driven IO)
异步IO(Asynchronous IO)
这五大模型各有特点,适用于不同的场景和需求。在实际应用中,需要根据具体情况选择合适的IO模型来提高程序的性能和效率。
参考文章:浅谈5种IO模型
消息通信的同步异步与进程线程的同步异步在概念和应用上存在一些差异,以下是具体的分析和归纳:
🌹 消息通信的同步与异步
定义:
特点:
🌹 进程与线程的同步与异步
定义:
特点:
🌹总结
消息通信的同步异步与进程线程的同步异步在概念上有所相似,但应用场景和关注点有所不同。消息通信主要关注消息发送和接收之间的同步或异步关系,而进程线程的同步异步则关注任务之间的执行顺序和并发性。在实际应用中,需要根据具体的需求和场景来选择合适的同步异步方式。
fcntl
函数是计算机中用于文件描述符控制的一种函数,它允许对已打开的文件性质进行修改。以下是fcntl
函数的用法介绍:
🥕 一、函数声明
#include
#include
int fcntl(int fd, int cmd, ...);
fcntl`函数接受三个参数:
cmd
的值,可能需要一个int arg
或struct flock *lock
作为第三个参数。🥕 二、功能介绍
fcntl
函数根据cmd
参数的值执行不同的操作,主要有以下几种:
F_DUPFD:
arg
的最小且仍未使用的文件描述符,并复制参数fd
的文件描述符。F_GETFD/F_SETFD:
fd
联合的close-on-exec标志。F_GETFL/F_SETFL:
F_GETLK/F_SETLK/F_SETLKW:
F_GETOWN/F_SETOWN:
🥕 三、返回值
cmd
的值,fcntl
可能返回不同的值。errno
以指示错误。🥕四、使用实例
在网络编程中,fcntl
常被用于将文件描述符设置为非阻塞模式,以便在数据未就绪时不会阻塞进程。以下是一个简单的示例,展示了如何使用fcntl
将标准输入设置为非阻塞模式:
#include
#include
#include
#include
int main(void) {
int flags, n;
//char buf[10];
// 获取stdin的当前标志
flags = fcntl(STDIN_FILENO, F_GETFL);
if (flags == -1) {
perror("fcntl error");
return 1;
}
// 添加O_NONBLOCK标志
flags |= O_NONBLOCK;
// 设置stdin为非阻塞模式
if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1) {
perror("fcntl error");
return 1;
}
// 接下来的read调用将不会阻塞,如果数据未就绪,将返回-1并设置errno为EAGAIN或EWOULDBLOCK
// ...
while (1) {
char buf[1024] = {0};
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
if (read_size < 0) {
perror("read:");
sleep(1);
continue;
}
printf("input:%s\n", buf);
}
return 0;
}
🥕五、总结
fcntl
是一个功能强大的函数,它允许程序对文件描述符进行精细的控制。通过fcntl
,程序可以改变文件的性质、设置锁、更改异步IO行为等。在编写涉及文件操作或网络编程的程序时,fcntl
是一个值得了解和掌握的函数。
select
函数是一个用于I/O多路复用的系统调用,它允许程序监视多个文件描述符的状态变化(例如可读、可写或发生异常)。这对于实现高效的I/O操作,尤其是非阻塞I/O和服务器程序中的并发处理非常有用。
🥕 一、函数声明
在POSIX兼容的系统中,select
函数的声明通常如下:
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
🥕 二、参数说明
nfds
:指定被监听的文件描述符集合中最大文件描述符加1。通常设置为监听的文件描述符集合中的最大值加1。readfds
:指向一个文件描述符集合的指针,该集合中的文件描述符用于监视读状态变化。writefds
:指向一个文件描述符集合的指针,该集合中的文件描述符用于监视写状态变化。exceptfds
:指向一个文件描述符集合的指针,该集合中的文件描述符用于监视异常状态变化。timeout
:指向一个timeval
结构的指针,该结构指定了select
函数的超时时间。如果设置为NULL,则select
会无限期地等待。🥕 三、返回值
select
返回就绪的文件描述符的总数。errno
以指示错误。🥕 四、文件描述符集合
fd_set
是一个文件描述符集合,它通常通过一系列宏来操作,例如FD_ZERO
、FD_SET
、FD_CLR
和FD_ISSET
。这些宏定义在
头文件中。
🥕 五、使用实例
以下是一个简单的select
使用示例,用于监视标准输入(stdin)的可读状态:
#include
#include
#include
#include
#include
int main(void) {
fd_set readfds;
struct timeval tv;
int ret;
// 初始化文件描述符集合
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
// 设置超时时间为5秒
tv.tv_sec = 5;
tv.tv_usec = 0;
// 调用select函数
ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv);
if (ret == -1) {
perror("select error");
exit(EXIT_FAILURE);
} else if (ret == 0) {
printf("No data within 5 seconds.\n");
} else {
// 检查标准输入是否可读
if (FD_ISSET(STDIN_FILENO, &readfds)) {
// 读取数据...
// ...
printf("Data is available on stdin.\n");
}
}
return 0;
}
🥕六、select缺点
🥕七、总结
select
函数是一个强大的工具,允许程序同时监视多个文件描述符的状态变化。然而,在处理大量文件描述符时,select
可能会遇到性能瓶颈,因为它需要遍历所有被监视的文件描述符。在这种情况下,更现代的替代品(如poll
和epoll
)可能更适合。不过,对于许多常见的应用场景,select
仍然是一个简单而有效的解决方案。
#include
#include
#include
int main()
{
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(0, &read_fds);
for (;;)
{
printf("> ");
fflush(stdout);
int ret = select(1, &read_fds, NULL, NULL, NULL);
if (ret < 0)
{
perror("select");
continue;
}
if (FD_ISSET(0, &read_fds))
{
char buf[1024] = {0};
read(0, buf, sizeof(buf) - 1);
printf("input: %s", buf);
}
else
{
printf("error! invaild fd\n");
continue;
}
FD_ZERO(&read_fds);
FD_SET(0, &read_fds);
}
return 0;
}
poll
函数是Linux中用于I/O多路复用的系统调用之一,类似于select
函数,但它在处理大量文件描述符时更加灵活和高效。以下是poll
函数的详细用法介绍:
🥕 一、函数声明
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
🥕二、参数说明
fds:指向pollfd
结构数组的指针,每个pollfd
结构表示一个需要监视的文件描述符。
POLLIN
(数据可读)、POLLOUT
(数据可写)等。nfds:fds
数组中的元素数量,即要监视的文件描述符的数量。
timeout:指定poll
函数的超时时间(以毫秒为单位)。
poll
调用将阻塞等待,直到至少有一个文件描述符上发生事件。poll
调用将立即返回,无论是否有文件描述符上发生事件。poll
调用将在指定的毫秒数内等待,如果在此期间没有文件描述符上发生事件,则超时返回。🥕 三、返回值
errno
以指示错误。🥕 四、使用步骤
pollfd
结构数组,并设置每个元素的fd
和events
字段。poll
函数,传入pollfd
结构数组、数组长度和超时时间。poll
函数的返回值,以及每个pollfd
结构的revents
字段,以确定哪些文件描述符上的事件已经就绪。🥕 五、示例代码
以下是一个简单的示例代码,展示了如何使用poll
函数来监视标准输入(stdin)的可读状态:
#include
#include
#include
#include
#include
int main(void) {
struct pollfd fds[1];
char buffer[1024];
int n;
// 初始化pollfd结构数组
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN; // 监视可读事件
// 调用poll函数,设置超时时间为5秒
int timeout = 5000; // 5秒转换为毫秒
n = poll(fds, 1, timeout);
if (n == -1) {
perror("poll error");
exit(EXIT_FAILURE);
} else if (n == 0) {
printf("No data within 5 seconds.\n");
} else {
// 检查标准输入是否可读
if (fds[0].revents == POLLIN) {
// 读取数据
ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0'; // 添加字符串终止符
printf("Read %zd bytes: %s", bytes_read, buffer);
} else {
perror("read error");
}
}
}
return 0;
}
🥕 六、总结
poll
函数提供了一种高效的方式来监视多个文件描述符的状态变化。与select
函数相比,poll
在处理大量文件描述符时更加灵活和高效,因为它没有select
函数中的文件描述符数量限制。然而,对于非常大的文件描述符集合,更现代的替代品(如epoll
)可能更加适合。
#include
#include
#include
int main()
{
struct pollfd poll_fd;
poll_fd.fd = 0;
poll_fd.events = POLLIN;
for (;;)
{
int ret = poll(&poll_fd, 1, 5000);
if (ret < 0)
{
perror("poll");
continue;
}
if (ret == 0)
{
printf("poll timeout\n");
continue;
}
if (poll_fd.revents == POLLIN)
{
char buf[1024] = {0};
read(0, buf, sizeof(buf) - 1);
printf("stdin:%s", buf);
}
}
}
I/O多路转接之epoll
epoll是Linux内核为处理大批量文件描述符而作的改进的poll,是Linux下多路复用IO接口select/poll的增强版本。它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。以下是关于epoll的详细解释:
一、epoll的特点和优势
高效性:
事件表
,当文件描述符的状态发生变化时,内核会通知用户空间,从而减少了不必要的系统调用。边缘触发
(Edge Triggered)模式,使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,进一步提高应用程序效率。无限制的文件描述符数量:
内存使用优化:
二、epoll的接口和工作原理
epoll提供了三个主要的系统调用:epoll_create()、epoll_ctl()和epoll_wait()。
epoll使用红黑树
来管理待检测的文件描述符集合,这使得在添加、删除和查找文件描述符时具有对数时间复杂度,从而提高了效率。
三、epoll的使用场景
当需要同时监视多个文件描述符(如sockets、文件、管道等)上的事件,并在有事件发生时通知应用程序进行相应的处理时,epoll是一个非常好的选择。特别是在处理大量并发连接但只有少量活跃连接的情况下,epoll的性能优势尤为明显。
四、总结
epoll作为Linux内核提供的一种高效的多路复用IO接口,其特点在于高效性、无限制的文件描述符数量和内存使用优化。通过epoll,可以方便地同时监视多个文件描述符上的事件,并在事件发生时进行高效的处理。因此,在高并发、低延迟的应用场景中,epoll是一个值得考虑的解决方案。
当使用epoll
进行I/O多路复用时,主要涉及到三个系统调用:epoll_create()
, epoll_ctl()
, 和 epoll_wait()
。以下是这些函数的原型和参数解释:
😉 1. epoll_create()
函数原型:
int epoll_create(int size);
int epoll_create1(int flags); // 这是 epoll_create 的一个扩展版本
参数解释:
size
(对于epoll_create
):这个参数是告诉内核这个监听的数目最大值。注意这个值只是内核初始分配内部数据结构的大小,并不是限制。在Linux 2.6.8及以后的版本中,这个参数被忽略,但是为了代码的可移植性,通常还是传递一个合适的大小值,比如 1。flags
(对于epoll_create1
):这是一个位掩码,用于修改epoll
实例的行为。常用的标志有EPOLL_CLOEXEC
(当执行exec()
函数时,关闭文件描述符)。返回值:
作用:
创建一个新的epoll
实例,并返回一个文件描述符,用于后续通过epoll_ctl()
添加、修改或删除要监视的文件描述符,以及通过epoll_wait()
等待文件描述符上的事件。
😉 2. epoll_ctl()
函数原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数解释:
epfd
:由epoll_create()
或epoll_create1()
返回的文件描述符。op
:操作码,可以是以下三种之一:
EPOLL_CTL_ADD
:注册新的文件描述符到epfd
。EPOLL_CTL_MOD
:修改已经注册的文件描述符的监听事件。EPOLL_CTL_DEL
:从epfd
中注销一个文件描述符。fd
:需要添加、修改或删除的文件描述符。event
:指向epoll_event
结构的指针,描述了要监听的事件和与之关联的数据。返回值:
作用:
用于向epoll
实例中添加、修改或删除要监视的文件描述符及其相关的事件。
😉 3. epoll_wait()
函数原型:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数解释:
epfd
:由epoll_create()
或epoll_create1()
返回的文件描述符。events
:指向epoll_event
结构数组的指针,用于存储从内核返回的事件。maxevents
:告诉内核这个events
数组有多大,这个值不能大于创建epoll_create
时的size
。timeout
:等待超时时间(毫秒),-1 表示永远等待。返回值:
作用:
等待在epfd
上注册的文件描述符上的事件。当这些事件中的任何一个发生时,epoll_wait()
将返回,并将所有触发的事件存储在events
数组中。
注意:在使用epoll
时,还需要了解epoll_event
结构体,它描述了注册到epoll
实例中的事件和与之关联的数据。这个结构体通常包含两个成员:events
(表示要监听的事件类型)和data
(用户定义的数据,通常用于在事件触发时识别是哪个文件描述符触发了事件)。
epoll_event
结构体是 epoll
机制中用于注册、修改和接收文件描述符上事件的重要数据结构。它定义在
头文件中,并用于 epoll_ctl()
函数中注册感兴趣的事件和 epoll_wait()
函数中接收已触发的事件。
🍚epoll_event
结构体的定义通常如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
🍚 结构体成员解释
events:
EPOLLIN
:当相应的文件描述符可读时触发。EPOLLOUT
:当相应的文件描述符可写时触发。EPOLLPRI
:当相应的文件描述符有优先读取数据可读时触发(不常用)。EPOLLERR
:当相应的文件描述符发生错误时触发。EPOLLHUP
:当相应的文件描述符被挂起时触发(如 TCP 连接被对方关闭)。EPOLLET
:设置文件描述符为边缘触发(Edge Triggered)模式。默认是水平触发(Level Triggered)模式。data:
ptr
:一个指向任意类型数据的指针。fd
:一个文件描述符。这通常用于存储与事件关联的文件描述符,但请注意,这与 epoll_event
结构体中的 events
成员中引用的文件描述符不同。u32
和 u64
:无符号的 32 位和 64 位整数。你可以使用这些字段来存储自定义的整数数据。🍚 使用方法
在 epoll_ctl()
调用中,你会创建一个 epoll_event
结构体实例,并设置其 events
成员为你感兴趣的事件类型,以及 data
成员为你想要关联的数据。然后,你将这个结构体的指针传递给 epoll_ctl()
。
在 epoll_wait()
调用中,你会传递一个 epoll_event
结构体数组以及它的大小给该函数。当 epoll_wait()
返回时,它会更新这个数组中的 epoll_event
结构体实例,以反映实际触发的事件。然后,你可以遍历这个数组,检查每个 epoll_event
结构体的 events
成员来确定哪些事件被触发了,并使用 data
成员来获取与事件关联的数据。
为了使用epoll
来监控标准输入(通常是文件描述符0,即stdin),我们可以编写一个简单的程序来演示epoll_create()
, epoll_ctl()
, 和 epoll_wait()
的使用。以下是一个简单的C程序示例:
#include
#include
#include
#include
#include
#include
#define MAX_EVENTS 10
int main(void) {
int epfd, nfds;
struct epoll_event ev, events[MAX_EVENTS];
// 创建一个 epoll 实例
epfd = epoll_create1(0);
if (epfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
// 配置要监控的事件
ev.events = EPOLLIN; // 监听可读事件
ev.data.fd = STDIN_FILENO; // 标准输入的文件描述符
// 向 epoll 实例添加监控的文件描述符
if (epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev) == -1) {
perror("epoll_ctl: add");
exit(EXIT_FAILURE);
}
// 等待事件发生
for (;;) {
nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); // 阻塞等待
if (nfds == -1) {
perror("epoll_wait");
break;
}
// 遍历所有触发的事件
for (int n = 0; n < nfds; ++n) {
if (events[n].data.fd == STDIN_FILENO) {
// 读取标准输入
char buffer[1024];
ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
// 去掉换行符并打印
buffer[bytes_read] = '\0';
char *newline = strchr(buffer, '\n');
if (newline) *newline = '\0';
printf("Read from stdin: %s\n", buffer);
} else if (bytes_read == 0) {
printf("EOF reached on stdin\n");
break;
} else {
perror("read");
break;
}
}
}
}
// 关闭 epoll 文件描述符
close(epfd);
return 0;
}
在这个程序中,我们首先使用epoll_create1()
创建一个epoll
实例。然后,我们使用epoll_ctl()
添加一个事件来监听标准输入(stdin)的可读事件。在无限循环中,我们使用epoll_wait()
等待事件发生。当标准输入上有数据可读时,我们读取这些数据并打印出来。如果读取到文件结束符(EOF),或者读取操作失败,我们退出循环。最后,我们关闭epoll
文件描述符并退出程序。
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长