課程內容#
理解兩者的真正含義及區別,能回答以上問題
兩者的字面意思#
上班
- 阻塞:上班時堵車了,等或者使用其他方法,總之要去上班
- 非阻塞:上班堵車了,直接不去了
老媽讓買醬油
- 阻塞:沒有醬油了,問媽要不換別的,或者換別的買
- 非阻塞:沒有醬油了,不買了
讓小明寫報告
- 阻塞:小明寫的時候,等他寫完
- 非阻塞:不用乾等小明寫,之後再告知結果 [自己問小明 - 同步 / 小明主動告訴我 - 異步]
燒開水
- 阻塞:燒開水的過程中,你不能幹其他事情,只能站那等水開
- 非阻塞:燒開水的過程中,你可以幹其他事情,比如去客廳看看電視
- 參考同步異步、阻塞非阻塞極簡解釋——CSDN
引入#
- 回顧 open 函數中的 O_NONBLOCK
- 一切皆文件,所以一切都可以阻塞 / 非阻塞
- 分析寫文件的過程
- 打開→write→關閉
- 其中,write 屬於系統調用,具體過程如下
- 數據→內核→磁碟:內核拷貝數據,放在緩衝區 [塊緩衝],清刷緩衝時,調度 IO 設備,找到 inode 和 block,將數據寫入磁碟
- 第一步數據→內核是用戶態,第二步內核→磁碟是內核態,存在轉換
- 參考怎樣去理解 Linux 用戶態和內核態?—— 知乎
- NON_BLOCK 設置的是用戶態 [數據→內核 或者 內核→數據] 的過程
- 大部分的阻塞是在從內核拿數據出來
- 內核→磁碟的過程實際是阻塞的
- 為了方便上層應用快速返回寫的狀態,內核將數據寫到磁碟的過程,普通用戶不需要感知
- 內核拷貝完數據,程序返回,普通用戶就認為寫成功了,但數據很有可能還在緩衝區 [緩衝 IO]
- 數據→內核→磁碟:內核拷貝數據,放在緩衝區 [塊緩衝],清刷緩衝時,調度 IO 設備,找到 inode 和 block,將數據寫入磁碟
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 上生效
- 為了兼容性,盡量別用,盡量使用比較公共的功能
- 指定 select 阻塞著等待每個文件描述符就緒的時間間隔
- 返回值
- 返回此時所有集合中文件描述符 [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 號文件;而不是某個函數、某個操作
- ① 沒有 sleep
- [PS]
- 阻塞:可能浪費時間
- 非阻塞:可能浪費資源
- 一般用於大型的程序、高並發伺服器
對 0 號文件的 select#
- 拷貝自 man 手冊 ——man select 裡的 EXAMPLE
- select 可以感知到 I/O 的到來
- 運行效果
- 終端輸入 ls,程序結束後還會執行 ls
- 因為緩衝區的內容沒有被取走 [如 scanf],最終被 zsh 接走了
- select 在這監控的時間間隔裡發生了阻塞
- 在所有的用戶層面的程序裡,所謂的阻塞就是睡覺 [sleep ()]
- 阻塞終止條件:ready、signal 中斷、超時
- [延伸應用]
- 給阻塞的地方設置一個定時,超時則使用默認值
- 避免異常情況阻塞過久 [SSH 主機不可達...]
- 更人性化
- 給阻塞的地方設置一個定時,超時則使用默認值
附加知識點#
- I/O 感知的方式可以有很多種,其中內核完全知道 IO 的到來
思考點#
- scanf 的時候,在終端輸入了回車,是將數據放入了緩衝區,還是清空緩衝區給了 scanf 呢?
Tips#
- 課程預告:多進程 [fork...]
- 可以考慮 cp 的實現
- 考察文件的讀寫操作
- 必須阻塞