1. 程式人生 > >IO模型分析

IO模型分析

何為同步與非同步

在LNMP的生態當中我們基本上用到的是同步操作.例如PHP的file_get_contents函式就是一個典型的同步任務.而Node.js當中的回撥模式是一個典型的非同步模式如Promise.

同步

同步可以理解為:傳送一個系統呼叫,並等待系統呼叫的返回.

非同步

非同步可以理解為:傳送一個系統呼叫,不等待系統返回可以繼續處理當前的任務,等待系統處理完畢的回撥.

不同的IO模型

阻塞與非阻塞強調的是當前程序或者是執行緒的狀態.

阻塞IO

在這裡插入圖片描述

從圖中可知,資料經歷兩種變化.
1.資料從沒有準備狀態到準備完成
2.從核心態拷貝資料到使用者態.

非阻塞IO

在這裡插入圖片描述

發起recvfrom呼叫後核心在準備資料.
同步不斷的輪訓,發現數據準備完成後,將資料從核心態拷貝到使用者態.

多路複用IO

在這裡插入圖片描述
IO多路複用:無需使用polling或者多執行緒就可以"同時"(指時間段)處理多個檔案描述符.但是需要注意從核心讀取資料還是同步的.
經典的select與epoll都是IO多路複用.圖中描述的是select模型.epoll詢問方式和select有很大不同.

epoll與select的區別

  • epoll要比select高效
  • select的支援最大檔案描述符有限,epoll沒有限制

select:每次返回準備好的檔案描述數量.呼叫方自己遍歷所有的FD,判斷是否準備好,準備好然後再讀資料.
select的最大的問題,我們不知道那些FD是否準備好,只能去遍歷.
假設有100個FD,只有1個準備好,可想而知,相當於一個順序查詢,效率低下.
因此高效的epoll就出現了,只告訴呼叫方準備好的FD


    fd_set readfds, writefds;
    FD_ZERO(&readfds);
    FD_ZERO(&writefds);
    for (int i=0; i < read_fd_count; i++)
      FD_SET(my_read_fds[i], &readfds);
    for (int i=0; i < write_fd_count; i++)
      FD_SET(my_write_fds[i], &writefds);

    struct timeval timeout;
    timeout.
tv_sec = 3; timeout.tv_usec = 0; int num_ready = select(FD_SETSIZE, &readfds, &writefds, NULL, &timeout); if (num_ready < 0) { perror("error in select()"); } else if (num_ready == 0) { printf("timeout\n"); } else { for (int i=0; i < read_fd_count; i++) if (FD_ISSET(my_read_fds[i], &readfds)) printf("fd %d is ready for reading\n", my_read_fds[i]); for (int i=0; i < write_fd_count; i++) if (FD_ISSET(my_write_fds[i], &writefds)) printf("fd %d is ready for writing\n", my_write_fds[i]); }

epoll不是POSIX標準.但卻在Linux當中被廣泛使用.
epoll:只返會準備好的FD.

#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;

/* Code to set up listening socket, 'listen_sock',
  (socket(), bind(), listen()) omitted */

epollfd = epoll_create1(0);//建立epoll
if (epollfd == -1) {
   perror("epoll_create1");
   exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {//監聽指定的listen_sock
   perror("epoll_ctl: listen_sock");
   exit(EXIT_FAILURE);
}

for (;;) {
   nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);//迴圈取事件,放入event事件
   if (nfds == -1) {
       perror("epoll_wait");
       exit(EXIT_FAILURE);
   }

   for (n = 0; n < nfds; ++n) {
       if (events[n].data.fd == listen_sock) {
           conn_sock = accept(listen_sock,
                              (struct sockaddr *) &addr, &addrlen);//接收客戶端連線
           if (conn_sock == -1) {
               perror("accept");
               exit(EXIT_FAILURE);
           }
           setnonblocking(conn_sock);// ET模式下需要設定為非阻塞模式
           ev.events = EPOLLIN | EPOLLET;// ET模式下
           ev.data.fd = conn_sock;
           if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                       &ev) == -1) {//新增客戶端的事件到當前的epoll
               perror("epoll_ctl: conn_sock");
               exit(EXIT_FAILURE);
           }
       } else {
           do_use_fd(events[n].data.fd); //使用新的準備好的檔案描述符不停接收資料,並在這裡記錄當前讀取,寫入的資料
       }
   }
}

訊號驅動IO

在這裡插入圖片描述

訊號驅動IO無需等待資料準備好,系統會發送訊號SIGIO回撥handler,然後讀取資料.
這時候IO是同步讀取,從核心態到使用者態.

非同步IO

在這裡插入圖片描述

真正的非同步IO,從圖中很明顯看出資料已經被拷貝到了使用者態,這是與訊號IO模型最大的區別.

IO模型比較

在這裡插入圖片描述

從圖中我可以得出:

  • 非同步IO:不會導致執行緒阻塞.
  • 同步IO:執行緒一直阻塞到資料IO操作完成.