linux --- 常見的IO模型與fcnl和select函數說明

IO 模型

當我們寫一個程序,他要求外設進行輸入或者輸出時,那麼在內核進行處理數據的這個時間中,我們的程序都在做了什麼事呢?

IO過程

  1. 等待IO就緒,也就是想要獲取的資源已經準備好了,可以進行操作了。

在這裏插入圖片描述

  1. 將需要操作的數據,拷貝到緩衝區中

阻塞IO

阻塞IO,在內核態中把數據準備完成之前,用戶態中的調用程序會一直處於等待的狀態。(什麼也不幹,就等着內核告訴自己可以了)

之前所使用的那些套接字中,他們的默認IO 方式都是阻塞IO
在這裏插入圖片描述
特點:

  1. 阻塞IO中,用戶空間阻塞的時間大小取決於內核空間的處理數據的速度
  2. 在等待的過程中,執行流是被掛起的,這就造成了對CPU的利用率低
  3. 程序編寫的流程簡單

非阻塞IO

非阻塞IO,我們在程序中是使用循環詢問的方式,一直問內核態數據有沒有準備完成

  • 準備完成,內核態進行拷貝數據,返回成功的指示
  • 沒有準備完成,內核態返回EWOULDBLOCK錯誤碼

在這裏插入圖片描述
特點:

  1. 非阻塞IO對CPU的利用率相比阻塞IO來說要高一點
  2. 循環調用,直到IO請求完成
  3. IO準備就緒的時候,到拷貝數據之間不一定滿足實時性
  4. 程序流程複雜

區別:在資源不可用的情況下,就看系統的調用是否立即返回

  • 立即返回,非阻塞IO
  • 沒有返回,阻塞IO

信號驅動IO

信號驅動IO,內核態中把數據準備完成之後,使用之前定義的SIGIO信號通知應用程序進行IO操作。

流程:

  1. 程序收到一個IO信號
  2. 內核調用這個IO信號的自定義處理函數(signal),在這個自定義的處理函數中發起IO 調用

在這裏插入圖片描述
特點:

  1. IO準備就緒,到數據的拷貝這個過程中,程序的實時性增加了
  2. 不需要重複發起IO調用,但是需要在程序中增加自定義信號函數的處理邏輯
  3. 程序比較複雜,流程多了。

異步IO

異步IO,當內核中把程序拷貝完成時,再通知應用程序(信號驅動是告訴應用程序何時可以開始拷貝數據)

在這裏插入圖片描述
特點:

  1. 異步IO模型中,數據拷貝的過程也被內核完成了
  2. 自定義的信號處理函數,使得程序流程變的複雜
  3. 和阻塞IO差不多,實時性沒有太大的影響

同步通信異步通信

之前在多線程中,有一個同步與互斥的概念,這裏同步的意思是:讓多個執行流之間可以合理的訪問臨界資源

而這裏的同步與異步中的同步是指:當程序發出一個調用的時候,在沒有得到結果之前,這個調用就不會返回。當調用返回的時候,就一定是得到了一個結果

異步是指:當程序發出一個調用,這個調用就直接返回了,所以沒有返回結果。等到內核中完成了拷貝的操作,再通過一些方式來通知調用者(信號),或者通過回調函數來處理這個調用。

二者的區別
數據拷貝的過程是否由程序來完成

  • 是程序來完成的:同步
  • 是內核來完成的:異步

是否是由調用者來等待調用結果

  • 是,則爲同步
  • 不是,則爲異步

IO 多路轉接

IO多路轉接,可以同時等待多個文件描述符的就緒狀態
在這裏插入圖片描述
作用: IO多路轉接可以完成大量描述符的監控,所監控的事件主要有:可讀事件可寫事件異常事件

當使用多路轉接IO的時候,每當多路轉接的接口處發現了一個就緒的文件描述符的時候,就會通知相應的進程,讓進程對這個描述符進行操作,其他描述符繼續進行監控。

好處 : 避免了其他進程對沒有就緒的文件描述符進行操作,從而陷入阻塞狀態

非阻塞IO 中的輪詢過程

  • 所使用的函數接口fcntl(),默認是阻塞IO
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );
  1. fd,文件描述符
  2. cmd,不同的cmd的值,會產生不同的作用

在這裏插入圖片描述
寫一個程序,他的功能就是獲取輸入的數據。

  1. 先使用自定義的SetNoBlock函數,將 0 -- 標準輸入的文件描述符設置爲非阻塞的狀態
  2. 使用while(1)死循環來模擬一個非阻塞調用的過程,用read來當做內核態中的拷貝數據的過程
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>

using namespace std;

void SetNoBlock(int fd)
{
    //獲得文件描述符的狀態標識
    int ret = fcntl(fd,F_GETFL);
    if(ret < 0)
    {
        perror("fcntl");
        return ;
    }
    //設置文件描述符的狀態,並加上一個非阻塞狀態
    fcntl(fd,F_SETFL,ret | O_NONBLOCK);
}

int main()
{
    SetNoBlock(0);//0 -- 標準輸入描述符

    while(1)
    {
        char buf[1024] = {'\0'};
        ssize_t read_size = read(0,buf,sizeof(buf) - 1);
        if(read_size < 0)
        {
            perror("read");
            sleep(3);

            continue;
        }

        cout<<"輸入了 : "<<buf<<endl;
    }

    return 0;
}

程序運行結果:
在這裏插入圖片描述

IO 多路轉接中的select

作用: 用程序來對多個文件描述符的狀態進行監控,如果有描述符準備就緒,就返回該描述符,讓用戶對這個描述符進行操作

  1. 將用戶所要使用的文件描述符拷貝到內核中,讓內核幫助用戶進行監控
  2. 如果內核發現某個文件描述符已經就緒,就返回這個文件描述符
  3. 用戶對這個文件描述符進行操作

函數接口:

  • select接口
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
    fd_set *exceptfds, struct timeval *timeout);
  1. nfds:取值爲所監控的最大的文件描述符的數值 + 1(最大爲1024),可以提高select的監控效率
  2. fd_set :是一個結構體,在結構體內部是一個fds_bits數組,他的使用可以看成是一個位圖,一共1024位,對應着1024個文件描述符

在這裏插入圖片描述
在這裏插入圖片描述

typedef long int __fd_mask;

#define __FD_SETSIZE	1024

#define __NFDBITS   (8 * (int) sizeof (__fd_mask)

__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];

所以說,數組中元素的個數 = 1024 / (8 * sizeof(long int) ) --》 也就是16個元素

fds_bits數組的大小(位) = (1024 / (8 * sizeof(long int) )) * sizeof(long int ) * 8 = 1024

fd_set接口

void FD_CLR(int fd, fd_set *set);   // 用來清除描述詞組set中相關fd 的位

int FD_ISSET(int fd, fd_set *set);  // 用來測試描述詞組set中相關fd 的位是否爲真

void FD_SET(int fd, fd_set *set);   // 用來設置描述詞組set中相關fd的位

void FD_ZERO(fd_set *set);       	// 用來清除描述詞組set的全部位
  1. rdset,wrset,exset分別對應於需要檢測的可讀文件描述符的集合可寫文件描述符的集 合異常文件描述符的集合;
  2. timeout,超時時間

在這裏插入圖片描述
在這裏插入圖片描述
5. 函數的返回值

在這裏插入圖片描述

注意:select 返回的時候,會將沒有就緒的文件描述符從集合中去除,只返回就緒的文件描述符

所以我們需要循環給fd_set結構體進行初始化操作

#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>

int main()
{
    fd_set read_fds;
    
    timeval time;

    while(1)
    {
        //清除 fd_set 的所有位
        FD_ZERO(&read_fds);
        // 設置 0 標準輸入 文件描述符
        FD_SET(0,&read_fds);
        
        printf("> ");
        fflush(stdout);
        
        //沒有超時時間
        //int ret = select(1,&read_fds,NULL,NULL,NULL);
        
        time.tv_sec = 3;
        time.tv_usec = 0;
        //設置超時時間
        printf("time = %d\n",time.tv_sec);
        int ret = select(0 + 1,&read_fds,NULL,NULL,&time);
        if(ret < 0)
        {
            perror("select");
            continue;
        }
        else if(ret == 0)
        {
            printf("timeout time : %d\n",time.tv_sec);
            sleep(1);
            continue;
        }

        //判斷 0 標準輸入文件描述符是否爲真
        if(FD_ISSET(0,&read_fds))
        {
            //就緒
            char buf[1024] = {'\0'};
            read(0,buf,sizeof(buf)-1);
            printf("輸入 : %s\n",buf);
        }
        else 
        {
            printf("無效的文件描述符 \n");
            continue;
        }

    }
    return 0;
}

在使用select 的定時機制的時候,需要注意

select 中的第五個參數,就是timeval結構體,但是所傳的結構體是一個實時性的結構體,在發生阻塞進行計時的時候,會對結構體中的數據進行減法操作

如果我們只用select函數判斷一次,那麼我們怎麼設置都可以(在select調用之前);但是如果我們需要進行循環判斷,那麼我們必須在循環的內部進行賦值操作,不可以再循環外面。

如果我們在循環的外面只賦值一次,那麼就只有第一次超時是有效的,其他的時候程序會一直卡在timeout這塊
在這裏插入圖片描述
通過打印結構體的時間進行驗證
在這裏插入圖片描述

寫在循環內部的timeval賦值後的結果:
在這裏插入圖片描述

select 的優缺點

優點:

  1. select 遵循的是posix標準,可以跨平臺操作
  2. select 對於超時的時間可以控制在微秒

缺點:

  1. select 是輪詢遍歷的,監控的效率會隨着文件描述符的增多而下降
  2. select 可以監控的文件描述符是有上限的(1024),取決於內核中__FD_SETSIZE宏的值
  3. select 在監控文件描述符的時候,需要將集合拷貝到內核當中;當監控的文件描述符就緒的時候,同樣會從內核拷貝到用戶空間中,他的效率會受到影響
  4. select 在返回就緒的文件描述符的時候,會把集合中沒有就緒的文件描述符刪除掉,導致第二次在監控的時候還需要重新添加
  5. select 無法直接查看那個文件描述符已經就緒,需要手動通過返回事件的集合去判斷
  6. select 的超時機制,如果在循環判斷的情況下,每次調用之前都需要更新一下時間。因爲在計時的時候,這個結構體中的時間是會變的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章