阻塞与非阻塞IO(Input/Output)
阻塞与非阻塞IO(Input/Output)是计算机操作系统中两种不同的文件或网络通信方式。它们的主要区别在于程序在等待IO操作完成时的行为。
阻塞IO(Blocking IO)
在阻塞IO模式下,当一个线程发起IO请求(如读取数据或写入数据)时,它会一直等待直到IO操作完成。这意味着在等待数据的过程中,该线程不能执行其他任务,因此被称为“阻塞”。这种模式下,每个IO请求都需要一个独立的线程来处理,因为每个线程都会被阻塞直到IO操作完成。
优点:
- 编程模型简单,易于理解和实现。
- 对于IO操作较少的应用,资源消耗相对较小。
缺点:
- 线程利用率低,因为线程在等待IO操作完成时不能执行其他任务。
- 可扩展性差,随着并发IO请求的增加,需要更多的线程来处理,这会导致资源消耗增加
常见的阻塞IO函数:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
非阻塞IO(Non-blocking IO)
在非阻塞IO模式下,当一个线程发起IO请求时,它会立即返回,不会等待IO操作完成。如果IO操作尚未完成,系统会返回一个特定的错误码(如EWOULDBLOCK)。线程可以继续执行其他任务,直到IO操作准备好,此时系统会通知线程,线程可以再次尝试进行IO操作
优点:
- 线程可以处理多个IO请求,提高了线程的利用率。
- 可扩展性好,适用于高并发的IO操作场景。
缺点:
- 编程模型复杂,需要处理更多的错误码和状态检查。
- 需要额外的机制(如事件循环、IO多路复用等)来管理多个IO请求
fcntl()函数:
函数头文件:
#include <unistd.h>
#include <fcntl.h>
函数原型:
int fcntl(int fd, int cmd, ... /* arg */ );
函数参数:
fd: 要操作的文件描述符
cmd: 一个命令代码,它指定了要执行的操作
arg: 一个可变参数,它的类型和值取决于 cmd 的值
cmd命令:
F_DUPFD(int):复制文件描述符。指定新文件描述符的最小值
F_GETFD(void):获取文件描述符的状态。
F_SETFD(int):设置文件描述符的状态。可以设置 FD_CLOEXEC 标志
F_GETFL(void):获取文件描述符的状态标志。
F_SETFL(int):设置文件描述符的状态标志。常用的标志有 O_NONBLOCK 设置文件描述符为非阻塞模式
和 O_APPEND设置文件描述符为追加模式。
F_GETLK(struct flock*):获取文件锁的状态。用于存储当前的锁信息
F_SETLK(struct flock*):设置文件锁。
F_SETLKW(struct flock*):设置文件锁,并等待锁就绪。
...
函数返回值:
成功:
F_DUPFD:新的文件描述符
F_GETFD:文件描述符标志的值
F_GETFL:文件描述符状态的值
...
失败:返回-1,并设置错误原因
在Linux中,可以通过将文件描述符设置为非阻塞模式来实现非阻塞IO。这可以通过fcntl()
系统调用完成
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
IO多路复用
基本思想:由内核来监控多个文件描述符是否可以进行I/O操作,如果有就绪的文件描述符,将结果 告知给用户进程,则用户进程在进行相应的I/O操作
IO多路复用的主要优点:
提高资源利用率:通过减少线程或进程的数量,降低了操作系统在线程上下文切换和维护线程状态上的开销。
提高系统性能:线程不需要在等待IO操作时被阻塞,而是可以继续执行其他任务,直到IO操作准备好。
可扩展性:可以处理大量的并发连接
IO多路复用的机制:
a.select多路复用I/O
在Linux中,select()
系统调用是一种实现IO多路复用的方法,它允许程序同时监视多个文件描述符,以确定是否有文件描述符已经准备好进行IO操作。这意味着程序可以等待多个输入或输出通道,当至少有一个通道可以进行操作时,select()
调用会返回。
函数描述:
函数头文件:
#include <sys/select.h>
函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds,struct timeval *timeout);
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
函数参数:
nfds:最大文件描述符加1
readfds:指向读文件描述符集合的指针。
如果某个文件描述符在返回时有数据可读,该描述符会在这个集合中被设置
writefds:指向写文件描述符集合的指针。
如果某个文件描述符在返回时可以无阻塞地写入数据,该描述符会在这个集合中被设置
exceptfds:指向异常文件描述符集合的指针。通常用于检测异常条件,如错误状态
timeout:指定等待时间,可以指定为NULL,表示无限期等待直到有文件描述符就绪
函数返回值:
成功:返回已经就绪的文件描述符的个数。如果设置timeout,超时就会返回0
失败:-1,并设置errno确定错误原因
操作文件描述符集合的宏定义:
从文件描述符集合set中移除文件描述符fd
void FD_CLR(int fd, fd_set *set);
检查文件描述符fd是否在文件描述符集合set中
int FD_ISSET(int fd, fd_set *set);
返回值:如果fd在集合set中,返回非零值;否则返回零。
将文件描述符fd添加到文件描述符集合set中
void FD_SET(int fd, fd_set *set);
初始化或清空文件描述符集合set
void FD_ZERO(fd_set *set);
工作原理:
-
内核监控: 当
select()
被调用时,内核会接管文件描述符集合,并开始监控这些文件描述符的状态。 -
数据准备: 当有网络数据到达或文件准备好被读取时,内核会检测到这些状态的变化。
-
通知进程: 一旦某个文件描述符就绪,内核会通知调用
select()
的进程。 -
返回就绪集合:
select()
返回时,会返回就绪的文件描述符数量,并更新传递给它的文件描述符集合,以反映哪些文件描述符已经就绪。 -
用户空间处理: 应用程序在用户空间检查哪些文件描述符就绪,并进行相应的IO操作。
-
超时处理: 如果在指定的超时时间内没有任何文件描述符就绪,
select()
将返回0,表示超时。
示例代码:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#define NFDS 0
int main()
{
// 可读的文件描述符集合
fd_set readfds;
// 清空可读的文件描述符集合
FD_ZERO(&readfds);
// 添加要监控的文件描述符到集合中
FD_SET(0,&readfds);
// 设置超时时间
struct timeval timeout = {.tv_sec=3,.tv_usec=0};
int result;
struct timeval timeout_bak;
fd_set readfds_bak;
while(1)
{
timeout_bak = timeout;
readfds_bak = readfds;
int result=select(NFDS+1, &readfds_bak,NULL,NULL, &timeout_bak);
if(result==-1)
{
perror("select");
exit(EXIT_FAILURE);
}
else if(result==0)
{
printf("Overtime\n");
}
else if(result>0)
{
for(int i=0;i<result;i++)
{
if(FD_ISSET(0,&readfds)){
char buf[128]={0};
fgets(buf,sizeof(buf),stdin);
printf("buf:%s\n",buf);
}
}
}
}
}
注意事项:
select()
的所有文件描述符集合都需要在用户空间和内核空间之间复制,这可能会带来性能开销。select()
有一个限制,即可以监视的文件描述符数量通常有一个上限(通常是1024),这取决于具体的系统配置。- 超时时间在
select()
返回后可能会被修改,如果需要再次使用原来的超时时间,需要备份timeval
结构体。
b.poll多路复用I/O
与 select()
类似,但它没有文件描述符的数量限制。这意味着 poll()
可以处理任意数量的文件描述符,只要系统资源(如内存)允许。这使得 poll()
在处理大量并发连接时更为有效
poll():使用一个 pollfd
结构体数组来跟踪文件描述符和事件。每个 pollfd
结构体包含一个文件描述符、期望的事件(events
)和实际发生的事件(revents
)。这种方式使得 poll()
在处理大量文件描述符时更为高效,因为它直接操作文件描述符数组,而不需要复制整个集合
函数描述:
函数头文件:
#include <poll.h>
函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
函数参数:
fds:指向pollfd结构体数组的指针,该数组包含了需要监控的文件描述符。
nfds:fds 数组的长度。
timeout:超时时间,以毫秒为单位。如果设置为-1,则poll()会无限期等待;如果设置为0,则不会等待,立即返回
fd:需要监控的文件描述符。
events:需要监控的事件类型,可以是POLLIN(可读)、POLLOUT(可写)、POLLERR(错误)、POLLHUP(挂起)等。
revents:实际发生的事件,由poll()函数填充
函数返回值:
成功:返回revents就绪的文件描述符的数量,如果超时,返回0
失败:返回-1,并设置error错误原因
工作原理:
-
监控多个文件描述符:
poll()
通过监控多个文件描述符的状态,允许程序在单个线程内处理多个输入输出源。这通过使用一个文件描述符集合来实现,其中每个文件描述符都可以设置为监控读、写或异常事件。 -
事件驱动:
poll()
采用事件驱动的方式工作。程序指定哪些事件(如可读、可写)感兴趣,poll()
则等待这些事件发生。当任何一个文件描述符上发生了感兴趣的事件时,poll()
调用返回。 -
水平触发(Level Triggered):
poll()
默认的工作方式是水平触发(LT),这意味着只要文件描述符的状态没有改变,poll()
会持续报告该文件描述符就绪,即使数据已经被读取或写入。 -
超时处理: 如果在指定的超时时间内没有任何文件描述符就绪,
poll()
将返回0,表示超时。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#define MAX_FDS 10
int main()
{
struct pollfd fds[MAX_FDS]={0};
// 将标准输入文件描述符封装成struct pollfd结构体对象
struct pollfd fd={
.fd = 0,
.events=POLLIN
};
fds[0]=fd;
nfds_t nfds = 1;
int result;
while(1)
{
result = poll(fds, nfds, 2000);
if(result==-1)
{
perror("poll");
exit(EXIT_FAILURE);
}
else if(result == 0)
{
printf("overtime\n");
}
else if(result>0)
{
for(int i=0;i<nfds;i++)
{
if(fds[i].revents == POLLIN){
char buf[128]={0};
fgets(buf,sizeof(buf),stdin);
printf("buf:%s\n",buf);
}
}
}
}
return 0;
}
c. epoll多路复用I/O
epoll
相对于select
与poll
有较大的不同,主要是针对前面两种多路复用 IO 接口的不足
epoll能够更有效地处理大量并发文件描述符,因为它不需要在每次调用时复制整个文件描述符集合,也不会受到文件描述符数量的限制
epoll
使用了两种主要的数据结构:红黑树和双向链表。
-
红黑树:
epoll
在内核中使用红黑树来高效地管理文件描述符。每个注册到epoll
的文件描述符都会存储在这颗树中,以便快速地进行查找、插入和删除操作。 -
双向链表:
epoll
维护了一个就绪列表,这是一个双向链表,用于存储所有准备好进行 IO 操作的文件描述符。当一个文件描述符变得可读或可写时,它会被添加到这个链表中
事件通知机制:
-
回调机制(callback):当一个文件描述符的状态发生变化(例如,从不可读变为可读),
epoll
会通过回调机制将这个事件添加到就绪列表中。这种机制避免了对所有注册的文件描述符进行轮询,从而提高了效率。 -
就绪列表:
epoll
维护了一个就绪列表,这是一个双向链表,用于存储所有准备好进行 IO 操作的文件描述符。当epoll_wait()
被调用时,它只需检查这个就绪列表,而不是遍历整个文件描述符集合。 -
边缘触发(ET)模式:在边缘触发模式下,
epoll
只在文件描述符的状态发生变化时通知应用程序。这意味着应用程序必须读取或写入所有可用的数据,直到遇到 EAGAIN 错误,否则该文件描述符不会被再次报告为就绪。
函数描述:
1.epoll创建函数
函数头文件:
#include <sys/epoll.h>
函数原型:
int epoll_create(int size);
函数参数:
size:这个参数是一个提示,告诉内核预计要监控的文件描述符数量
从Linux 2.6.8开始,这个参数被忽略,epoll_create可以处理的文件描述符数量只受限于系统资源,如内存和内核配置。
函数返回值:
成功:返回一个非负的文件描述符
失败:返回-1,并设置errno以指示错误原因
ENOMEM(内存不足)
ENFILE(打开的文件数量超过限制)
2.epoll控制函数
函数头文件:
#include <sys/epoll.h>
函数原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *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 *//* 与文件描述符相关联的数据 */
};
函数参数:
epfd:由 epoll_create() 调用返回的 epoll 实例的文件描述符。
op:要执行的操作类型,可以是以下值之一:
EPOLL_CTL_ADD:添加新的文件描述符到epoll实例。
EPOLL_CTL_MOD:修改已经添加到epoll实例中的文件描述符的监控事件。
EPOLL_CTL_DEL:从epoll实例中删除文件描述符。
fd:要监控的文件描述符。
event:指向epoll_event结构体的指针,该结构体定义了文件描述符上感兴趣的事件
events:指定文件描述符上感兴趣的事件类型,可以是以下值的组合:
EPOLLIN:文件描述符可读。
EPOLLOUT:文件描述符可写。
EPOLLERR:文件描述符发生错误。
EPOLLHUP:文件描述符被挂起。
EPOLLET:设置边缘触发模式(默认是水平触发模式)。
data:用于传递与文件描述符相关联的特定数据,可以是文件描述符本身或其他自定义数据
函数返回值:
成功时:返回0
失败时:返回-1,并设置errno以指示错误原因
3.epoll等待函数
函数头文件:
#include <sys/epoll.h>
函数原型:
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
函数参数:
epfd:由epoll_create()调用返回的epoll实例的文件描述符。
events:指向epoll_event结构体数组的指针,该数组用于从内核接收发生的事件。
maxevents:events数组的最大长度,即可以返回的最大事件数量。
timeout:等待时间,单位为毫秒。如果设置为-1,则无限期等待直到至少有一个文件描述符就绪;如果设置为 0,则不等待,立即返回。
函数返回值:
成功:返回就绪的文件描述符的数量,即 events 数组中填充的事件数量
失败:返回-1,并设置errno以指示错误原因
EBADF(无效的文件描述符)
EFAULT(无效的内存访问)
EINTR(被信号打断)...
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#define MAX_FDS 10
#define MAX_EVENTS 10
int main()
{
//创建epoll使用 epoll_create 函数
int epfd = epoll_create(1);
if(epfd==-1)
{
perror("epoll_create");
exit(EXIT_FAILURE);
}
//添加文件描述符使用 epoll_ctl 函数将文件描述符注册到epoll
struct epoll_event event;
event.events = EPOLLIN; // 监听可读事件
event.data.fd = 0; // 关联文件描述符
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);
if(ret == -1)
{
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
//等待 使用 epoll_wait 函数等待事件
int nfds;
struct epoll_event events[MAX_FDS];
while(1)
{
nfds = epoll_wait(epfd, events,MAX_EVENTS,2000);
if(nfds==-1)
{
perror("epoll_wait");
exit(EXIT_FAILURE);
}
else if(nfds == 0)
{
printf("overtime\n");
}
else if(nfds>0)
{
// epoll_wait返回就绪文件描述符的个数
for(int i=0;i<nfds;i++)
{
if(events[i].data.fd==0){
char buf[128]={0};
fgets(buf,sizeof(buf),stdin);
printf("buf:%s\n",buf);
}
}
}
}
return 0;
}
结语:
无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力