【linux编程】Linux的五种IO模型:阻塞式IO、非阻塞式IO、IO多路复用、信号驱动式IO和异步IO

Linux IO模型是指在Linux系统中,用户进程和内核进行输入输出操作的方式。输入输出操作通常涉及到数据的传输和拷贝,这些操作会消耗系统资源和时间,因此选择合适的IO模型对于提高程序的性能和效率是非常重要的。

Linux系统为我们提供了五种可用的IO模型,分别是阻塞式IO、非阻塞式IO、IO多路复用、信号驱动式IO和异步IO。这些模型的作用是让应用程序能够更好地管理和处理输入输出操作。下面我将简要介绍这五种模型的概念、特点、优缺点和示例代码。

阻塞式IO

阻塞式IO是最传统的一种IO模型,它的特点是在读写数据过程中会发生阻塞现象。当用户进程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户进程就会处于阻塞状态,用户进程交出CPU。当数据就绪之后,内核会将数据拷贝到用户进程,并返回结果给用户进程,用户进程才解除阻塞状态。

阻塞式IO的优点是实现简单,编程方便,不需要额外的处理逻辑。阻塞式IO的缺点是效率低,用户进程在等待数据的过程中无法执行其他任务,浪费了CPU资源。阻塞式IO比较适合数据量小,响应时间短的场景。

下面是一个使用阻塞式IO的示例代码,它从标准输入读取一行数据,然后打印出来:

#include 
#include 

int main()
{
    char buf[1024];
    int n;
    n = read(STDIN_FILENO, buf, 1024); //阻塞式IO,等待用户输入
    if (n < 0)
    {
        perror("read error");
        return -1;
    }
    printf("read %d bytes: %s\n", n, buf); //打印读取的数据
    return 0;
}

运行结果:

hello
read 6 bytes: hello

非阻塞式IO

非阻塞式IO是一种改进的IO模型,它的特点是在读写数据过程中不会发生阻塞现象。当用户进程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户进程的请求,那么它马上就将数据拷贝到了用户进程,然后返回。

非阻塞式IO的优点是用户进程不会被阻塞,可以继续执行其他任务,提高了CPU的利用率。非阻塞式IO的缺点是用户进程需要不断地询问内核数据是否就绪,这样会导致CPU占用率非常高,而且实现复杂,编程困难。非阻塞式IO比较适合数据量大,响应时间长的场景。

下面是一个使用非阻塞式IO的示例代码,它从标准输入读取一行数据,然后打印出来:

#include 
#include 
#include 
#include 

int main()
{
    char buf[1024];
    int n;
    int flags;
    flags = fcntl(STDIN_FILENO, F_GETFL); //获取标准输入的文件描述符的标志位
    flags |= O_NONBLOCK; //设置非阻塞标志位
    fcntl(STDIN_FILENO, F_SETFL, flags); //设置标准输入的文件描述符的标志位
    while (1)
    {
        n = read(STDIN_FILENO, buf, 1024); //非阻塞式IO,立即返回结果
        if (n < 0)
        {
            if (errno == EAGAIN) //如果错误码是EAGAIN,表示数据还没有就绪
            {
                printf("no data available\n"); //打印提示信息
                sleep(1); //休眠1秒,模拟执行其他任务
                continue; //继续尝试读取数据
            }
            else //如果错误码是其他值,表示发生了其他错误
            {
                perror("read error"); //打印错误信息
                return -1;
            }
        }
        printf("read %d bytes: %s\n", n, buf); //打印读取的数据
        break; //跳出循环
    }
    return 0;
}

运行结果:

no data available
no data available
no data available
hello
read 6 bytes: hello

IO多路复用

IO多路复用是一种高效的IO模型,它的特点是可以同时监控多个文件描述符,提高了应用程序对输入输出操作的管理能力。当用户进程使用select、poll或epoll等系统调用时,它会将需要监控的文件描述符传递给内核,然后内核会在所有文件描述符中寻找就绪的文件描述符,并返回给用户进程。用户进程再根据就绪的文件描述符进行相应的读写操作。

IO多路复用的优点是可以使用一个线程或进程来处理多个文件描述符,避免了多线程或多进程的开销,提高了系统的并发性和可伸缩性。IO多路复用的缺点是需要额外的系统调用来管理文件描述符,而且在数据拷贝阶段仍然是阻塞的。IO多路复用比较适合网络编程,比如服务器端的并发处理。

示例代码,它使用select来监控标准输入和一个TCP套接字的状态,并根据状态进行读写操作。代码如下:

#include 
#include 
#include 
#include 
#include 
#include 

#define PORT 8888 //定义服务器端口号
#define MAXLINE 1024 //定义缓冲区大小

int main()
{
    int sockfd; //定义套接字文件描述符
    int nready; //定义就绪的文件描述符的数量
    int n; //定义读写的字节数
    char buf[MAXLINE]; //定义缓冲区
    struct sockaddr_in servaddr; //定义服务器地址结构体
    fd_set rset; //定义读集合
    int maxfd; //定义最大的文件描述符

    sockfd = socket(AF_INET, SOCK_STREAM, 0); //创建一个TCP套接字
    if (sockfd < 0)
    {
        perror("socket error");
        return -1;
    }

    servaddr.sin_family = AF_INET; //设置地址族为IPv4
    servaddr.sin_port = htons(PORT); //设置端口号为8888,注意使用网络字节序
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //设置服务器IP地址为本地回环地址,注意使用网络字节序

    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) //连接服务器
    {
        perror("connect error");
        return -1;
    }

    printf("connect to server success\n");

    FD_ZERO(&rset); //清空读集合

    while (1)
    {
        FD_SET(STDIN_FILENO, &rset); //将标准输入加入读集合
        FD_SET(sockfd, &rset); //将套接字加入读集合
        maxfd = (STDIN_FILENO > sockfd) ? STDIN_FILENO : sockfd; //计算最大的文件描述符

        nready = select(maxfd + 1, &rset, NULL, NULL, NULL); //等待读集合中的文件描述符就绪,返回就绪的文件描述符的数量,NULL表示无限等待
        if (nready < 0)
        {
            perror("select error");
            return -1;
        }

        if (FD_ISSET(STDIN_FILENO, &rset)) //如果标准输入就绪
        {
            n = read(STDIN_FILENO, buf, MAXLINE); //从标准输入读取数据
            if (n < 0)
            {
                perror("read error");
                return -1;
            }
            else if (n == 0) //如果读到文件末尾,表示用户输入了Ctrl+D
            {
                printf("exit\n");
                break; //跳出循环,结束程序
            }
            write(sockfd, buf, n); //将读取的数据写入套接字,发送给服务器
        }

        if (FD_ISSET(sockfd, &rset)) //如果套接字就绪
        {
            n = read(sockfd, buf, MAXLINE); //从套接字读取数据
            if (n < 0)
            {
                perror("read error");
                return -1;
            }
            else if (n == 0) //如果读到文件末尾,表示服务器关闭了连接
            {
                printf("server closed\n");
                break; //跳出循环,结束程序
            }
            write(STDOUT_FILENO, buf, n); //将读取的数据写入标准输出,显示给用户
        }
    }

    close(sockfd); //关闭套接字
    return 0;
}

这个示例代码的功能是实现一个简单的回显客户端,它可以和服务器进行双向通信,用户可以在标准输入输入数据,然后发送给服务器,服务器会原样返回数据,客户端再将数据显示在标准输出。同时,客户端也可以接收服务器主动发送的数据,并显示在标准输出。当用户输入Ctrl+D或者服务器关闭连接时,客户端会退出。

这个示例代码的关键点是使用select函数来同时监控标准输入和套接字的状态,然后根据状态进行读写操作。这样可以避免使用阻塞式IO时的等待问题,提高了程序的效率和响应性。

信号驱动式IO

信号驱动式IO是一种基于信号的IO模型,它的特点是可以让用户进程在数据就绪时收到一个信号,然后再进行读写操作。当用户进程使用sigaction等系统调用时,它会告诉内核在数据就绪时向它发送一个SIGIO信号,并指定一个信号处理函数。当内核检测到数据就绪时,它会向用户进程发送一个SIGIO信号,并执行用户进程指定的信号处理函数。用户进程在信号处理函数中可以进行读写操作。

信号驱动式IO的优点是用户进程不需要主动轮询数据是否就绪,而是被动地接收内核的通知,节省了CPU资源。信号驱动式IO的缺点是信号机制本身的复杂性和不可靠性,而且在数据拷贝阶段仍然是阻塞的。信号驱动式IO比较适合实时性要求高的场景。

下面是一个使用信号驱动式IO的示例代码,它使用sigaction来注册一个信号处理函数,当标准输入有数据就绪时,接收SIGIO信号,并在信号处理函数中读取数据并打印出来:

#include 
#include 
#include 
#include 

void sigio_handler(int signo) //信号处理函数
{
    char buf[1024];
    int n;
    n = read(STDIN_FILENO, buf, 1024); //从标准输入读取数据
    if (n < 0)
    {
        perror("read error");
        return;
    }
    printf("read %d bytes: %s\n", n, buf); //打印读取的数据
}

int main()
{
    int flags;
    struct sigaction act; //用于设置信号处理函数的结构体
    act.sa_handler = sigio_handler; //设置信号处理函数为sigio_handler
    act.sa_flags = 0; //设置信号处理标志位为0
    sigemptyset(&act.sa_mask); //清空信号屏蔽集
    if (sigaction(SIGIO, &act, NULL) < 0) //注册SIGIO信号的处理函数
    {
        perror("sigaction error");
        return -1;
    }

    flags = fcntl(STDIN_FILENO, F_GETFL); //获取标准输入的文件描述符的标志位
    flags |= O_ASYNC; //设置异步标志位
    fcntl(STDIN_FILENO, F_SETFL, flags); //设置标准输入的文件描述符的标志位
    fcntl(STDIN_FILENO, F_SETOWN, getpid()); //设置标准输入的文件描述符的属主为当前进程

    while (1) //循环等待信号到来
    {
        pause(); //暂停进程,直到收到一个信号
    }
    return 0;
}

运行结果:

hello
read 6 bytes: hello

异步IO

异步IO是一种最高级的IO模型,它的特点是可以让用户进程在数据拷贝完成时收到一个通知,然后再进行读写操作。当用户进程使用aio_read等系统调用时,它会告诉内核在数据拷贝完成时向它发送一个信号或者执行一个回调函数,并指定一个异步IO控制块。当内核检测到数据就绪时,它会将数据拷贝到用户进程指定的缓冲区,并向用户进程发送一个信号或者执行一个回调函数。用户进程在信号处理函数或者回调函数中可以直接访问缓冲区中的数据。

异步IO的优点是用户进程不需要等待数据的拷贝,而是在数据的拷贝完成后才被通知,这样可以最大程度地减少用户进程的阻塞时间,提高了IO的效率和性能。异步IO的缺点是实现非常复杂,编程难度高,而且不是所有的操作系统和文件系统都支持异步IO。异步IO比较适合对IO的吞吐量和延迟有严格要求的场景。

下面是一个使用异步IO的示例代码,它使用aio_read来发起一个异步读取操作,当标准输入有数据拷贝完成时,接收SIGUSR1信号,并在信号处理函数中访问缓冲区中的数据并打印出来:

#include 
#include 
#include 
#include 
#include 

struct aiocb cb; //异步IO控制块

void aio_handler(int signo, siginfo_t *info, void *context) //信号处理函数
{
    if (info->si_signo == SIGUSR1) //如果信号是SIGUSR1
    {
        printf("read %d bytes: %s\n", cb.aio_nbytes, (char *)cb.aio_buf); //打印读取的数据
    }
}

int main()
{
    char buf[1024]; //缓冲区
    struct sigaction act; //用于设置信号处理函数的结构体
    act.sa_sigaction = aio_handler; //设置信号处理函数为aio_handler
    act.sa_flags = SA_SIGINFO; //设置信号处理标志位为SA_SIGINFO,表示使用sa_sigaction而不是sa_handler
    sigemptyset(&act.sa_mask); //清空信号屏蔽集
    if (sigaction(SIGUSR1, &act, NULL) < 0) //注册SIGUSR1信号的处理函数
    {
        perror("sigaction error");
        return -1;
    }

    cb.aio_fildes = STDIN_FILENO; //设置异步IO的文件描述符为标准输入
    cb.aio_buf = buf; //设置异步IO的缓冲区为buf
    cb.aio_nbytes = 1024; //设置异步IO的字节数为1024
    cb.aio_offset = 0; //设置异步IO的偏移量为0
    cb.aio_sigevent.sigev_notify = SIGEV_SIGNAL; //设置异步IO的通知方式为信号
    cb.aio_sigevent.sigev_signo = SIGUSR1; //设置异步IO的通知信号为SIGUSR1
    cb.aio_sigevent.sigev_value.sival_ptr = &cb; //设置异步IO的通知信号的附加信息为异步IO控制块的地址

    if (aio_read(&cb) < 0) //发起一个异步读取操作
    {
        perror("aio_read error");
        return -1;
    }

    while (1) //循环等待信号到来
    {
        pause(); //暂停进程,直到收到一个信号
    }
    return 0;
}

运行结果:

hello
read 6 bytes: hello

你可能感兴趣的:(算法,linux,运维)