Linux的五种IO模型

本文内容整理至以下资料:

[1] Linux五种IO模型https://blog.csdn.net/z_ryan/article/details/80873449

[2] select() - Unix, Linux System Call https://www.tutorialspoint.com/unix_system_calls/_newselect.htm

[3] Multiplexed I/0 with poll() https://www.linuxtoday.com/blog/multiplexed-i0-with-poll.html

[4] epoll() Tutorial – epoll() In 3 Easy Steps! https://www.suchprogramming.com/epoll-in-3-easy-steps/

[5] signal(SIGIO,XXXX)实例 https://blog.csdn.net/GZFStudy/article/details/51578570

[6] Linux 信号(signal)https://www.jianshu.com/p/f445bfeea40a

 

一、阻塞式I/O

进程调用一个recvfrom请求,如果数据没有准备好,则阻塞直到数据返回,然后将数据从内核空间复制到程序空间。

 

二、非阻塞式I/O

调用非阻塞的recvform系统调用后,进程不会被阻塞,如果数据还没准备好,此时会返回一个error。

进程在返回之后,可以处理其他的业务逻辑,过会儿再发起recvform系统调用。采用轮询的方式检查内核数据,直到数据准备好。再拷贝数据到进程,进行数据处理。

 

三、I/O复用(select,poll,epoll等)

特点:IO 多路复用的好处就在于单个进程就可以同时处理多个网络连接的IO

基本原理:不再由应用程序自己监视连接,取而代之由内核替应用程序监视文件描述符。下图来源为参考链接[1]

以select为例,当用户进程调用了select,那么整个进程会被阻塞。

与此同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。

(根据上述描述,select唤醒机制有点像互斥量mutex)

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int main(void) {
    fd_set rfds;
    struct timeval tv; // timeout
    int retval;

    /* Watch stdin (fd 0) to see when it has input. */
    FD_ZERO(&rfds);
    FD_SET(0, &rfds);
    /* Wait up to five seconds. */
    tv.tv_sec = 5;
    tv.tv_usec = 0;
    retval = select(1, &rfds, NULL, NULL, &tv);
    /* Don’t rely on the value of tv now! */

    if (retval == -1)
        perror("select()");
    else if (retval)
        printf("Data is available now.\n");
        /* FD_ISSET(0, &rfds) will be true. */
    else
        printf("No data within five seconds.\n");
    return 0;
}
Example Code of select()
#include <fcntl.h>
#include <stdio.h>
#include <sys/poll.h>
#include <sys/time.h>
#include <unistd.h>

/* For simplicity, all error checking has been left out */

int main(int argc, char ** argv) {
    int fd;
    char buf[1024];
    int i;
    struct pollfd pfds[2];
    fd = open(argv[1], O_RDONLY);

    while (1) {
        pfds[0].fd = 0;
        pfds[0].events = POLLIN;

        pfds[1].fd = fd;
        pfds[1].events = POLLIN;

        poll(pfds, 2, -1);
        if (pfds[0].revents & POLLIN) {
            i = read(0, buf, 1024);
            if (!i) {
                printf("stdin closed\n");
                return 0;
            }
            write(1, buf, i);
        }

        if (pfds[1].revents & POLLIN) {
            i = read(fd, buf, 1024);
            if (!i) {
                printf("file closed\n");
                return 0;
            }
            write(1, buf, i);
        }
    }
}
Example Code of poll()
#define MAX_EVENTS 5
#define READ_SIZE 10
#include <stdio.h>     // for fprintf()
#include <unistd.h>    // for close(), read()
#include <sys/epoll.h> // for epoll_create1(), epoll_ctl(), struct epoll_event
#include <string.h>    // for strncmp
 
int main()
{
  int running = 1, event_count, i;
  size_t bytes_read;
  char read_buffer[READ_SIZE + 1];
  struct epoll_event event, events[MAX_EVENTS];
  int epoll_fd = epoll_create1(0);
 
  if(epoll_fd == -1)
  {
    fprintf(stderr, "Failed to create epoll file descriptor\n");
    return 1;
  }
 
  event.events = EPOLLIN;
  event.data.fd = 0;
 
  if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event))
  {
    fprintf(stderr, "Failed to add file descriptor to epoll\n");
    close(epoll_fd);
    return 1;
  }
 
  while(running)
  {
    printf("\nPolling for input...\n");
    event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, 30000);
    printf("%d ready events\n", event_count);
    for(i = 0; i < event_count; i++)
    {
      printf("Reading file descriptor '%d' -- ", events[i].data.fd);
      bytes_read = read(events[i].data.fd, read_buffer, READ_SIZE);
      printf("%zd bytes read.\n", bytes_read);
      read_buffer[bytes_read] = '\0';
      printf("Read '%s'\n", read_buffer);
 
      if(!strncmp(read_buffer, "stop\n", 5))
        running = 0;
    }
  }
 
  if(close(epoll_fd))
  {
    fprintf(stderr, "Failed to close epoll file descriptor\n");
    return 1;
  }
  return 0;
}
Example Code of epoll()

 

四、信号驱动式I/O(SIGIO)

信号驱动IO,进程注册一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。下列代码来源为参考链接[6]

#include<signal.h>
#include<stdio.h>
#include <unistd.h>

//void (*sa_sigaction)(int, siginfo_t *, void *);
void handler(int signum, siginfo_t * info, void * context)
{
    if(signum == SIGIO)
        printf("SIGIO   signal: %d\n", signum);
    else if(signum == SIGUSR1)
        printf("SIGUSR1   signal: %d\n", signum);
    else
        printf("error\n");
    
    if(context)
    {
        printf("content: %d\n", info->si_int);
        printf("content: %d\n", info->si_value.sival_int);
    }
}

int main(void)
{
    //int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    struct sigaction act;
    
    /*
     struct sigaction {
     void     (*sa_handler)(int);
     void     (*sa_sigaction)(int, siginfo_t *, void *);
     sigset_t   sa_mask;
     int        sa_flags;
     };
     */
    act.sa_sigaction = handler;
    act.sa_flags = SA_SIGINFO;
    
    sigaction(SIGIO, &act, NULL);
    sigaction(SIGUSR1, &act, NULL);
    for(;;)
    {
        sleep(10000);
    }
    return 0;
}
Exanmple Code of Signal Receiver
#include <sys/types.h>
#include <signal.h>
#include<stdio.h>
#include <unistd.h>


int main(int argc, char** argv)
{
    if(4 != argc)
    {
        printf("[Arguments ERROR!]\n");
        printf("\tUsage:\n");
        printf("\t\t%s <Target_PID> <Signal_Number> <content>\n", argv[0]);
        return -1;
    }
    int pid = atoi(argv[1]);
    int sig = atoi(argv[2]);

    if(pid > 0 && sig > 0)
    {
        //int sigqueue(pid_t pid, int sig, const union sigval value);
        union sigval val;
        val.sival_int = atoi(argv[3]);
        printf("send: %d\n", atoi(argv[3]));
        sigqueue(pid, sig, val);
    }
    else
    {
        printf("Target_PID or Signal_Number MUST bigger than 0!\n");
    }
    
    return 0;
}
Example Code of signal trigger

 

五、异步I/O(POSIX的aio_系列函数)

  上述四种IO模型都是同步的。相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,就可以去处理其他的逻辑了,无论内核数据是否准备好,都会直接返回给用户进程,不会对进程造成阻塞。等到数据准备好了,内核直接复制数据到进程空间,然后从内核向进程发送通知,此时数据已经在用户空间了,可以对数据进行处理了。

 "Network I/O is not a priority for AIO because everyone writing POSIX network servers uses an event based, non-blocking approach. The old-style Java "billions of blocking threads" approach sucks horribly.

Disk write I/O is already buffered and disk read I/O can be prefetched into buffer using functions like posix_fadvise. That leaves direct, unbuffered disk I/O as the only useful purpose for AIO.

Direct, unbuffered I/O is only really useful for transactional databases, and those tend to write their own threads or processes to manage their disk I/O.

So, at the end that leaves POSIX AIO in the position of not serving any useful purpose. Don't use it." From https://stackoverflow.com/questions/87892/what-is-the-status-of-posix-asynchronous-i-o-aio

 

六、总结

以read()为例,

阻塞I/O:如果缓冲区里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据;

非阻塞I/O:如果缓冲区里没有数据,返回表示读取错误;(相较于阻塞I/O,可以while控制可以读取时就读取,其他时间做别的)

I/O复用:如果缓冲区没有数据,select()阻塞,直到收到数据,select()返回,执行其后的read();(相较于非阻塞I/O,不用程序检查是否可读取,由内核完成检查,并且epoll可以同时监控多个文件描述符)

信号驱动式I/O:进程注册一个信号处理函数。当数据准备好时,进程会收到一个SIGIO信号,进入对应函数进行处理。

异步I/O:异步非阻塞跟同步非阻塞的区别在于,即使缓冲区中有数据时,调用aio_read后,程序也会去处理其他逻辑;而同步非阻塞仅仅是缓冲区无数据时不阻塞。

 

下图来源为参考链接[1]

 

posted @ 2020-03-25 17:52  Han-helloWorld  阅读(220)  评论(0)    收藏  举报