引言
在 《UNIX 網(wǎng)絡(luò)編程》一書 135 頁的末尾提到關(guān)于 select 與 stdio 相關(guān)函數(shù)混用的問題唐础。這里我把它單獨(dú)拿出來,以一個(gè)簡單的例子說明一下。避免之后的使用中出現(xiàn)類似的問題贷洲。
問題根源
兩者的緩沖區(qū):
- 系統(tǒng) I/O 在內(nèi)核空間中存在緩沖,而在用戶空間沒有晋柱;
- stdio 系列函數(shù)除了在內(nèi)核空間中有緩存优构,在用戶空間也有緩沖;
緩沖區(qū)類型:
- 全緩沖(大部分緩沖都是這類型)
- 行緩沖(例如:stdio雁竞、stdout)
- 無緩沖(例如:stderr)
而具體的問題則是出現(xiàn)在 select 只會(huì)檢測內(nèi)核空間中的緩沖區(qū)钦椭,無法感知用戶空間中的緩沖區(qū)。當(dāng)數(shù)據(jù)從內(nèi)核空間復(fù)制到用戶空間的時(shí)候,即使該描述符對應(yīng)的緩存空間有數(shù)據(jù)彪腔,select 也不會(huì)再給通知侥锦。如圖:
image.png
示例
-
正常輸出
#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>
#include <string.h>
#define BUFFER 3
#define BUFFER_LEN (BUFFER - 1)
int main()
{
int n;
fd_set rset;
char buffer[BUFFER];
FD_ZERO(&rset);
for (;;)
{
FD_SET(fileno(stdin), &rset);
select(fileno(stdin) + 1, &rset, NULL, NULL, NULL);
n = read(fileno(stdin), buffer, BUFFER_LEN);
printf("讀取到:[%d] 字節(jié),內(nèi)容為:[%s]\n", n, buffer);
memset(buffer, 0, sizeof(buffer));
}
}
--- input
123456
--- output
讀取到:[2] 字節(jié)德挣,內(nèi)容為:[12]
讀取到:[2] 字節(jié)恭垦,內(nèi)容為:[34]
讀取到:[2] 字節(jié),內(nèi)容為:[56]
讀取到:[1] 字節(jié)盲厌,內(nèi)容為:[
]
?? Tips
我們分配 3 字節(jié)大小的緩沖區(qū)署照,然后再每次讀取玩緩沖中的數(shù)據(jù)之后,將緩沖中的數(shù)據(jù)清空吗浩,避免影響輸出建芙。當(dāng)我們輸入:123456 并按回車換行時(shí)(實(shí)際:123456\n),內(nèi)容依次輸出了懂扼。最后的 1 字節(jié)內(nèi)容就是最后的換行符禁荸。
我們分析一下從我們輸出完并按下回車到顯示時(shí),都發(fā)生了什么:
- 輸入回車之后阀湿,數(shù)據(jù)從用戶緩沖復(fù)制到了內(nèi)核緩沖(行緩沖)赶熟;
- select 檢測到 stdin 對應(yīng)的內(nèi)核緩沖有數(shù)據(jù)可讀的時(shí)候,解除阻塞陷嘴;
- read 函數(shù)取 2 個(gè)字節(jié)的數(shù)據(jù)到 buffer 中映砖;
- printf 將 buffer 中的數(shù)據(jù)顯示出來次和,并進(jìn)行下次循環(huán)抖韩,阻塞到 select端考;
- 由于內(nèi)核中還有數(shù)據(jù)未讀完径筏,select 再次解除阻塞棍厌,直至數(shù)據(jù)取完為止童本;
-
混用時(shí)的問題
#include <stdio.h>
#include <sys/select.h>
int main()
{
int n;
fd_set rset;
FD_ZERO(&rset);
for (;;)
{
FD_SET(fileno(stdin), &rset);
select(fileno(stdin) + 1, &rset, NULL, NULL, NULL);
n = getc(stdin);
printf("內(nèi)容為:[%c]\n", n);
}
}
---
intput: 123456
output: 內(nèi)容為:[1]
intput: 9
output: 內(nèi)容為:[2]
output: 內(nèi)容為:[3]
output: 內(nèi)容為:[4]
output: 內(nèi)容為:[5]
output: 內(nèi)容為:[6]
output: 內(nèi)容為:[
output: ]
output: 內(nèi)容為:[9]
我們發(fā)現(xiàn)輸出已經(jīng)出現(xiàn)問題了原探,我們繼續(xù)分析一下該問題是怎么造成的:
- 當(dāng)我們輸入 123456 之后柴底,數(shù)據(jù)由用戶空間緩沖復(fù)制到了內(nèi)核緩沖秒拔;
- select 檢測到有數(shù)據(jù)可讀莫矗,解除阻塞;
- getc 函數(shù)從用戶緩沖中取 1 字節(jié)數(shù)據(jù)砂缩,發(fā)現(xiàn)緩沖中無數(shù)據(jù)可讀作谚,于是將內(nèi)核中的數(shù)據(jù)復(fù)制到用戶緩沖,并取 1 字節(jié)作為輸出梯轻;
- 此時(shí)由于數(shù)據(jù)已經(jīng)全部復(fù)制到了用戶緩沖食磕,所以 select 進(jìn)入阻塞狀態(tài)(即使用戶空間的緩沖中有數(shù)據(jù)可讀);
- 當(dāng)輸出 9 并回車時(shí)喳挑,該數(shù)據(jù)又被復(fù)制到了內(nèi)核空間(行緩沖)彬伦,select 解除阻塞滔悉;
- getc 函數(shù)從用戶緩沖中取出 1 字節(jié)數(shù)據(jù)輸出(由于用戶緩沖中有數(shù)據(jù),所以 getc 便不會(huì)再從內(nèi)核中復(fù)制數(shù)據(jù))单绑;
- 由于內(nèi)核中有數(shù)據(jù)回官,所以 select 便再解除阻塞,getc 再取 1 字節(jié)直到 9 被復(fù)制到用戶緩沖并輸出為止搂橙;
?? Tips
仔細(xì)看最后的輸出歉提,你會(huì)發(fā)現(xiàn) 9 之后的換行符還留在用戶空間緩沖中,該數(shù)據(jù)只能等下次再有數(shù)據(jù)輸出到內(nèi)核空間中才會(huì)得到輸出区转。