Linux下的IO模型

阻塞与非阻塞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);
工作原理:
  1. 内核监控: 当select()被调用时,内核会接管文件描述符集合,并开始监控这些文件描述符的状态。

  2. 数据准备: 当有网络数据到达或文件准备好被读取时,内核会检测到这些状态的变化。

  3. 通知进程: 一旦某个文件描述符就绪,内核会通知调用select()的进程。

  4. 返回就绪集合select()返回时,会返回就绪的文件描述符数量,并更新传递给它的文件描述符集合,以反映哪些文件描述符已经就绪。

  5. 用户空间处理: 应用程序在用户空间检查哪些文件描述符就绪,并进行相应的IO操作。

  6. 超时处理: 如果在指定的超时时间内没有任何文件描述符就绪,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错误原因

 工作原理:

  1. 监控多个文件描述符poll() 通过监控多个文件描述符的状态,允许程序在单个线程内处理多个输入输出源。这通过使用一个文件描述符集合来实现,其中每个文件描述符都可以设置为监控读、写或异常事件。

  2. 事件驱动poll() 采用事件驱动的方式工作。程序指定哪些事件(如可读、可写)感兴趣,poll() 则等待这些事件发生。当任何一个文件描述符上发生了感兴趣的事件时,poll() 调用返回。

  3. 水平触发(Level Triggered)poll() 默认的工作方式是水平触发(LT),这意味着只要文件描述符的状态没有改变,poll() 会持续报告该文件描述符就绪,即使数据已经被读取或写入。

  4. 超时处理: 如果在指定的超时时间内没有任何文件描述符就绪,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相对于selectpoll有较大的不同,主要是针对前面两种多路复用 IO 接口的不足

epoll能够更有效地处理大量并发文件描述符,因为它不需要在每次调用时复制整个文件描述符集合,也不会受到文件描述符数量的限制

epoll 使用了两种主要的数据结构:红黑树和双向链表。

  • 红黑树epoll 在内核中使用红黑树来高效地管理文件描述符。每个注册到 epoll 的文件描述符都会存储在这颗树中,以便快速地进行查找、插入和删除操作。

  • 双向链表epoll 维护了一个就绪列表,这是一个双向链表,用于存储所有准备好进行 IO 操作的文件描述符。当一个文件描述符变得可读或可写时,它会被添加到这个链表中

事件通知机制:

  1. 回调机制(callback):当一个文件描述符的状态发生变化(例如,从不可读变为可读),epoll 会通过回调机制将这个事件添加到就绪列表中。这种机制避免了对所有注册的文件描述符进行轮询,从而提高了效率。

  2. 就绪列表epoll 维护了一个就绪列表,这是一个双向链表,用于存储所有准备好进行 IO 操作的文件描述符。当 epoll_wait() 被调用时,它只需检查这个就绪列表,而不是遍历整个文件描述符集合。

  3. 边缘触发(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;
}

结语:

无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力

 

Linux内核的IO模型主要包括阻塞IO、非阻塞IO、多路复用IO和异步IO。下面我将逐个介绍这些模型的特点。 1. 阻塞IO(Blocking IO):当应用程序发起一个IO操作后,内核会一直阻塞等待,直到IO操作完成才返回结果给应用程序。在这期间,应用程序是被阻塞的,无法进行其他操作。阻塞IO模型适用于对实时性要求不高的场景,简单易用,但会导致资源浪费。 2. 非阻塞IO(Non-Blocking IO):当应用程序发起一个IO操作后,内核会立即返回一个结果给应用程序,无论IO操作是否完成。如果IO操作还未完成,应用程序可以继续做其他事情,而不需要一直等待。应用程序可以通过轮询来检查IO操作的状态,直到操作完成。非阻塞IO模型可以提高系统的并发性能,但需要应用程序自己处理轮询逻辑。 3. 多路复用IO(Multiplexing IO):多路复用IO模型通过一个系统调用(如select、poll、epoll等)来同时监听多个IO事件,当有任意一个IO事件就绪时,内核会通知应用程序进行处理。这种模型避免了阻塞和轮询的问题,可以同时处理多个IO操作,提高系统的并发性能。 4. 异步IO(Asynchronous IO):异步IO模型中,应用程序发起一个IO操作后,可以立即返回继续执行其他操作,而不需要等待IO操作完成。当IO操作完成后,内核会通知应用程序,并返回结果。异步IO模型通过回调函数来处理IO完成的通知,相比于其他模型,可以更高效地处理大量的IO操作。 这些IO模型在不同的场景下有各自的优劣,选择合适的IO模型可以提高系统的性能和响应能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值