Bo2SS

Bo2SS

2 阻塞與非阻塞IO

  • 圖片

課程內容#

理解兩者的真正含義及區別,能回答以上問題

兩者的字面意思#

上班

  • 阻塞:上班時堵車了,等或者使用其他方法,總之要去上班
  • 非阻塞:上班堵車了,直接不去了

老媽讓買醬油

  • 阻塞:沒有醬油了,問媽要不換別的,或者換別的買
  • 非阻塞:沒有醬油了,不買了

讓小明寫報告

  • 阻塞:小明寫的時候,等他寫完
  • 非阻塞:不用乾等小明寫,之後再告知結果 [自己問小明 - 同步 / 小明主動告訴我 - 異步]

燒開水

  • 阻塞:燒開水的過程中,你不能幹其他事情,只能站那等水開
  • 非阻塞:燒開水的過程中,你可以幹其他事情,比如去客廳看看電視
  • 參考同步異步、阻塞非阻塞極簡解釋——CSDN

引入#

  • 回顧 open 函數中的 O_NONBLOCK
    • image-20210120101248947
    • 一切皆文件,所以一切都可以阻塞 / 非阻塞
  • 分析寫文件的過程
    • 打開→write→關閉
    • 其中,write 屬於系統調用,具體過程如下
      • 數據→內核→磁碟:內核拷貝數據,放在緩衝區 [塊緩衝],清刷緩衝時,調度 IO 設備,找到 inode 和 block,將數據寫入磁碟
      • NON_BLOCK 設置的是用戶態 [數據→內核 或者 內核→數據] 的過程
        • 大部分的阻塞是在從內核拿數據出來
      • 內核→磁碟的過程實際是阻塞的
        • 為了方便上層應用快速返回寫的狀態,內核將數據寫到磁碟的過程,普通用戶不需要感知
        • 內核拷貝完數據,程序返回,普通用戶就認為寫成功了,但數據很有可能還在緩衝區 [緩衝 IO]

fcntl#

操作文件描述符 [可以將文件變成非阻塞的]

  • man fcntl
  • 原型
    • 圖片
    • fd:文件描述符。最常見的文件描述符:0、1、2
    • cmd:操作方式
    • ... /* arg */
      • 可變參數
      • arg,指示該參數是前面參數 [cmd] 的參數,其含義取決於 cmd
  • 描述
    • 圖片
    • cmd 在圓括號裡指示是否有可變參數,最多一個
    • 可變參數類型一般是 int,使用用宏定義,本質是位掩碼,通過位運算改變狀態
    • 其中有一類 cmd:與【文件狀態標誌】相關
      • 圖片
      • 即可獲得或設置文件狀態 [如 O_NONBLOCK]
  • 返回值
    • 圖片
    • 觀察返回值,考慮程序中的判斷
    • 出錯都是返回 - 1,設置操作成功都是返回 0

select#

同步 I/O 多路復用 [接口]

  • man select
  • 原型
    • 圖片
    • nfds:文件描述符的數量
    • fd_set:文件描述符的集合
      • 可讀、可寫、異常
      • 底層是用數組是實現的
    • timeout:時間間隔
    • 四個宏用來操作集合,見後
  • 描述
    • 圖片
    • 允許程序去監控多個文件描述符,等待一個或多個文件描述符的 I/O 操作 "ready" 的情況
      • ready:就緒,可以對文件進行相應的 IO 操作,如沒有阻塞的讀,或足夠小的寫
    • 可監控的文件描述符數量小於 FD_SETSIZE [一般為 1024]
    • 圖片
    • 退出的時候,每個文件描述符集合會被修改,只留下狀態發生變化的文件描述符,起指示作用
      • 所以,如果循環使用 select,每次調用 select 前需要重新初始化每個集合
    • 集合可以是 NULL,說明該類事件中沒有文件被監控
    • 用來操作集合
      • FD_ZERO:清空一個集合
      • FD_SET:向一個集合添加一個文件描述符
      • FD_CLR:從一個集合移除一個文件描述符
      • FD_ISSET:判斷一個文件描述符的所屬集合,可用於 select 返回後,因為集合已被修改
    • nfds 的值是三個集合中數字最大的文件描述符加一
      • 文件描述符的索引是從 0 開始的
    • 圖片
    • struct timeval timeout
      • 指定 select 阻塞著等待每個文件描述符就緒的時間間隔
        • 這裡阻塞的含義就是單純的阻塞,不是阻塞 I/O 的阻塞
        • [個人理解] 同步 I/O 的多路復用的同步就體現在這
      • 三種停止阻塞的情況
        • 一個文件描述符就緒
        • 信號中斷 [kill]
        • 超時
      • 時間間隔不是精確的,很難做到真正的精確
        • 系統時鐘粒度、內核調度延遲
      • timeval 結構體中有兩個成員:秒、微秒
        • 如果兩者都為 0,就會立即返回,可用於輪詢 [polling]
        • 如果為 NULL,就會無期限等待
      • 圖片
      • timeout 更新功能只在 Linux 上生效
      • 為了兼容性,盡量別用,盡量使用比較公共的功能
  • 返回值
    • 圖片
    • 返回此時所有集合中文件描述符 [ready] 數量
      • 根據數量,再通過宏定義 FD_ISSET 詢問所有文件描述符的所在集合,來感知狀態變化
      • 0:時間過完了,也沒有感興趣的事件發生
      • -1:error,並設置 errno;此時集合不會改變,timeout 變成未定義的了

[PS]

  • select → poll → epoll,越來越高級
    • man poll
    • man epoll
    • 均與 select 做的任務相似

代碼演示#

實現使文件非阻塞和阻塞的接口#

  • common.h
    • 圖片
    • 兩個接口
  • head.h
    • 圖片
    • 頭文件的順序是有講究的,一般把自己本項目的頭文件放後面
    • 圖片
    • 參考#include 的路徑及順序——Google C++ 編程風格
  • common.c
    • 圖片
    • 設置 flag 時不能改變原有的 flag
    • 👉先 get,再通過位或 / 位與來添加 / 刪除 flag

非阻塞的 0 號文件#

  • 圖片
  • 常用文件 0、1、2 不用手動打開
    • 創建進程時自動打開這三個文件
    • 它們是繼承過來的
  • 編譯命令:gcc 1.test.c ../common/common.c  -I ../common/
    • 編譯記得添加 common.c 文件
  • 0 號文件設置成非阻塞後,關鍵變化在於 0 號文件的讀寫變成了非阻塞的
    • ① 沒有 sleep
      • 圖片
      • scanf 就是讀 0 號文件
      • 但沒有數據,直接走,不會阻塞著等 0 號文件有數據
    • ② 有 sleep
      • 圖片
      • 在 sleep 的 5 秒裡,在終端輸入數據 [+ 回車,行緩衝]
        • 進程暫停,不佔用 CPU
        • 但標準輸入流仍然打開著,用戶在終端輸入的數據會由內核傳給 0 號文件
          • 文件是內核維護的,sleep 的過程內核可以給文件寫數據
      • 在 sleep 結束後,scanf 讀走 0 號文件的數據
    • 【明確】非阻塞是指某個對象,比如 0 號文件;而不是某個函數、某個操作
  • [PS]
    • 阻塞:可能浪費時間
    • 非阻塞:可能浪費資源
      • 一般用於大型的程序、高並發伺服器

對 0 號文件的 select#

  • 拷貝自 man 手冊 ——man select 裡的 EXAMPLE
  • 圖片
  • select 可以感知到 I/O 的到來
  • 運行效果
    • image-20210120101405659
    • 終端輸入 ls,程序結束後還會執行 ls
    • 因為緩衝區的內容沒有被取走 [如 scanf],最終被 zsh 接走了
  • select 在這監控的時間間隔裡發生了阻塞
    • 在所有的用戶層面的程序裡,所謂的阻塞就是睡覺 [sleep ()]
    • 阻塞終止條件:ready、signal 中斷、超時
  • [延伸應用]
    • 給阻塞的地方設置一個定時,超時則使用默認值
      • 避免異常情況阻塞過久 [SSH 主機不可達...]
      • 更人性化

附加知識點#

  • I/O 感知的方式可以有很多種,其中內核完全知道 IO 的到來

思考點#

Tips#

  • 課程預告:多進程 [fork...]
  • 可以考慮 cp 的實現
    • 考察文件的讀寫操作
    • 必須阻塞

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。