?????????Socket 是網(wǎng)絡(luò)協(xié)議棧暴露給編程人員的 API,相比復(fù)雜的計算機網(wǎng)絡(luò)協(xié)議,API 對關(guān)鍵操作和配置數(shù)據(jù)進(jìn)行了抽象,簡化了程序編程。
? ? ? ? 本文講述的 socket 內(nèi)容源自 Linux man。本文主要對各 API 進(jìn)行詳細(xì)介紹,從而更好的理解 socket 編程。
select
遵循 POSIX.1 - 2008
1.庫
標(biāo)準(zhǔn) c 庫,libc, -lc
2.頭文件
<sys/select.h>
3.接口定義
int select(int nfds, fd_set *_Nullable restrict readfds,
fd_set *_Nullable restrict writefds,
fd_set *_Nullable restrict exceptfds,
struct timeval *_Nullable restrict timeout);
4.接口描述
? ? ? ? 首先,我們需要注意 select 只能監(jiān)聽少于?FD_SETSIZE(1024)? 個文件描述符,這在現(xiàn)在看來是非常不合理的,如果想不受這個限制,需要使用 poll 或者 epool。
? ? ? ? select 可以同時監(jiān)聽多個文件描述符,只要有一個文件描述符有操作需求時即返回。文件描述符有操作需求指的是可以馬上進(jìn)行相關(guān)的 I/O 操作,比如 read 或者少量的寫操作。
? ? fd_set
? ? ? ? 一個表示一組文件描述符的結(jié)構(gòu)體,根據(jù) POSIX 要求,結(jié)構(gòu)中最大文件描述符數(shù)量為 FD_SETSIZE。
? ? File descriptor set
? ? ? ? select() 接口重要的參數(shù)是 3 個文件描述符集合(以 fd_set 類型聲明),這允許調(diào)用者在指定的文件描述符集合上等待 3 種類型的事件。每個 fd_set 參數(shù)都可以是 NULL,只要沒有文件描述符集需要監(jiān)聽對應(yīng)的事件。
? ? ? ? 值得注意的是,一旦接口返回,每個文件描述符集都被更新,來指示哪些文件描述符就緒了。因此,如果在一個循環(huán)中使用 select(),集合必須每次調(diào)用前重新初始化。
? ? ? ? 文件描述符集的內(nèi)容可以使用以下宏來操作:
????????FD_ZERO()
? ? ? ? 這個宏用來清除集合中的所有文件描述符,是初始化文件描述符集的第一步。
? ? ? ? FD_SET()
? ? ? ? 這個宏用來向集合中添加文件描述符,如果文件描述符已經(jīng)存在,那么也不會報錯,只是不進(jìn)行任何操作。
? ? ? ? FD_CLR()
? ? ? ? 這個宏用來從集合中移除指定文件描述符,如果文件描述符不存在,則不進(jìn)行任何操作。
? ? ? ? FD_ISSET()
? ? ? ? select() 根據(jù)如下規(guī)則更新集合內(nèi)容:select() 調(diào)用結(jié)束后,F(xiàn)D_ISSET() 宏用來檢測指定文件描述符是否還位于集合中,如果存在則返回非 0 值,否則返回 0。
5.參數(shù)
(1)readfds
? ? ? ? 這個集合中的文件描述符用來監(jiān)測其是否已經(jīng)讀就緒。一個文件描述讀就緒指的是讀操作不會阻塞,特別的是,EOF 也算是讀就緒。
? ? ? ? select() 函數(shù)返回后,readfds 中只會保留讀就緒的文件描述符,其他都會被刪除。
(2)writefds
????????這個集合中的文件描述符用來監(jiān)測其是否已經(jīng)寫就緒。一個文件描述寫就緒指的是寫操作不會阻塞。不過即使一個文件描述符已經(jīng)寫就緒,但是大塊的寫操作可能也會阻塞。
? ? ? ? select() 函數(shù)返回后,writefds 中只會保留寫就緒的文件描述符,其他都會被刪除。
(3)eceptfds
? ? ? ? 這個集合中的文件描述符用來監(jiān)測其異常情況,一些異常情況的示例,在 poll() 的 POLLPRI 中會有討論。
? ? ? ? select() 返回后,exceptfds 中只保留發(fā)生異常情況的文件描述符。
(4)nfds
? ? ? ? 這個參數(shù)應(yīng)該被設(shè)置為 3 個集合中文件描述符的最大值加 1。
(5)timeout
? ? ? ? timeout 是一個 timeval 的結(jié)構(gòu),指定了 select() 等待文件描述符就緒的時間,這個接口會一直阻塞直到以下事件發(fā)生:
- 文件描述符就緒
- 調(diào)用被信號處理打斷
- timeout 超時
? ? ? ? 值得注意的是,timeout 值會向上(rounded up)近似到系統(tǒng)時鐘粒度,另外由于系統(tǒng)調(diào)度延遲,可能會導(dǎo)致阻塞間隔比 timeout 稍微大一些。
? ? ? ? 如果 timeout 的兩個成員都為 0,那么 select 會立即返回(通常用于輪詢)。
? ? ? ? 如果 timeout 是 NULL,select 會無限期等待直到有文件描述符就緒。
6.pselect()
? ? ? ? pselect() 系統(tǒng)調(diào)用能夠允許應(yīng)用更安全的等待文件描述符就緒或者信號發(fā)生。
? ? ? ? 它和 select() 是一樣的,除了以下幾個地方:
- select() 使用 timeval 結(jié)構(gòu)的 timeout,而 pselect() 使用 timespec 結(jié)構(gòu) 的timeout
- select() 可能會更新 timeout 參數(shù)來指示還有多少剩余時間,而 pselect() 不會
- select() 沒有信號屏蔽 sigmask 參數(shù),相當(dāng)于 pselect 的sigmask 參數(shù)為 NULL
? ? ? ? sigmask 是一個指向信號屏蔽的指針。如果它不為空,那么 pselect() 首先會使用它代替當(dāng)前的信號屏蔽,然后在進(jìn)行 select(),最后再恢復(fù)原來的信號屏蔽。如果是 NULL,那么 pselect() 調(diào)用過程并不會改變信號屏蔽值。
? ? ? ? 除了時間精度上的差異,下面兩端代碼等效:
ready = pselect(nfds, &readfds, &writefds, &exceptfds,
timeout, &sigmask);
????????
sigset_t origmask;
pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);
? ? ? ? 設(shè)計 pselect() 的原因是想要等待信號發(fā)生或者文件描述符就緒,那么就需要一個原子測試來解決數(shù)據(jù)競爭問題。比如,一個信號處理函數(shù)設(shè)置了一個標(biāo)志并返回,如果信號剛好在測試的附近到達(dá)導(dǎo)致數(shù)據(jù)競爭時, select() 后面測試這個標(biāo)志有可能無限期卡住。而 pselect() 允許先屏蔽信號,處理已經(jīng)發(fā)生的信號,然后使用指定 sigmask 來調(diào)用 pselect() ,避免了數(shù)據(jù)競爭。
????????timeout
? ? ? ? select() 的 timeout 結(jié)構(gòu)體定義如下:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
? ? ? ? pselect() 對應(yīng)的結(jié)構(gòu)體時 timespec。
Linux 系統(tǒng)上 select() 會修改 timeout 值來反映未睡眠的時間,其他實現(xiàn)不是這么做的。POSIX.1 認(rèn)為任何行為都是合法的。這就會導(dǎo)致 Linux 系統(tǒng)和其他系統(tǒng)之間的移植問題,所以,我們應(yīng)該認(rèn)為 timeout 在 select() 后是未知的值。
7.返回值
? ? ? ? 成功時,select() 和 pselect() 返回三個返回文件描述符集中的文件描述符總數(shù)(也就是 redfds、writefds、exceptfds 的中設(shè)置為 1 位數(shù))。返回值可以為 0,表示在有文件描述符就緒前 timeout 超時。
? ? ? ? 發(fā)生錯誤時,返回 -1,并設(shè)置errno 來指示錯誤類型。文件描述符集并不會被修改,timeout 值是未定義的。
? ? ? ? 錯誤值定義如下:
EBADF | 集合中存在不合法的文件描述符,比如已經(jīng)關(guān)閉的文件描述符或者發(fā)生錯誤的文件描述符),具體參見 BUGS |
EINTR | 捕獲了一個信號,具體參見 signal(7) |
EINVAL | nfds 是負(fù)值,或者超過了 RLIMIT_NOFILE 資源限制,具體參見getrlimit(2) |
EINVAL | timeout 中的數(shù)值不合法 |
ENOMEM | 沒有足夠內(nèi)存來分配內(nèi)部表 |
在其他 UNIX 系統(tǒng)上,如果系統(tǒng)無法分配內(nèi)核資源,select() 可能會返回 EAGAIN 錯誤而不是 ENOMEM。POSIX 為 poll() 定義了該錯誤,但是并沒有為 select() 定義。考慮到程序的移植性,應(yīng)該檢查 EGAIN 并重新調(diào)用,就行 EINTR 處理一樣。
8.注意
? ? ? ? <sys/time.h> 也提供了 fd_set 的定義,fd_set 是一個固定大小的緩沖區(qū),執(zhí)行 FD_CLR 和 FD_SET 傳入一個負(fù)值或者大于 FD_SETSIZE 的 fd 會導(dǎo)致不可預(yù)期的結(jié)果。此外,POSIX 要求 fd 是一個可用的文件描述符。
? ? ? ? select() 和 pselect() 操作不受 O_NONBLOCK 標(biāo)志的影響。
? ? ? ? self-pipe 小技巧
? ? ? ? 在沒有 pselect() 實現(xiàn)的系統(tǒng)上,可靠(更具有移植性)的信號捕捉可以通過 self-pipe 小技巧實現(xiàn)。這個技術(shù)在信號處理函數(shù)中向一個 pipe 中寫入 1 字節(jié),而該 pipe 的另一端由 select() 監(jiān)聽。為了防止?jié)M寫阻塞和空讀阻塞,pipe 的讀寫應(yīng)采用非阻塞 I/O 方式。
? ? ? ? 模擬 usleep
? ? ? ? 在 usleep 出現(xiàn)前,一些代碼使用 select() 來實現(xiàn)一種可移植的亞秒精度延遲,將所有集合設(shè)置為空,nfds 為 0,非空的 timeout值。
? ? ? ? select() 和 poll() 間通知的映射
? ? ? ? 在 linux 代碼樹中,我們可以發(fā)現(xiàn) select() 讀、寫、異常通知和 poll()/epoll() 事件通知之間的聯(lián)系:
#define POLLIN_SET (EPOLLRDNORM | EPOLLRDBAND | EPOLLIN |
EPOLLHUP | EPOLLERR)
/* Ready for reading */
#define POLLOUT_SET (EPOLLWRBAND | EPOLLWRNORM | EPOLLOUT |
EPOLLERR)
/* Ready for writing */
#define POLLEX_SET (EPOLLPRI)
/* Exceptional condition */
? ? ? ? 多線程應(yīng)用
? ? ? ? 如果一個線程通過 select() 監(jiān)聽的文件描述符被另一個現(xiàn)場關(guān)閉,那么結(jié)果是未知的。在一些 UNIX 系統(tǒng)上,select() 會停止阻塞并返回,告知文件描述符就緒(后續(xù)操作會出錯,除非剛好其他線程又打開了文件描述符并且就緒了)。在 Linux 及其他系統(tǒng)上,其他線程關(guān)閉文件描述符對 select() 沒有任何影響。總結(jié)起來,應(yīng)用如果依賴這些具體的行為的話,就會產(chǎn)生 bug。
? ? ? ? C 庫和內(nèi)核的差異
? ? ? ? Linux 內(nèi)核允許文件描述符集是任意大小的,由 nfds 的值來決定具體的大小。而 glibc 將fs_set 類型設(shè)置為固定值。參考 BUGS。
? ? ? ? 我們這里講述的 pselect() 接口是 glibc 實現(xiàn)的,底層系統(tǒng)調(diào)用名字是 pselect6(),系統(tǒng)調(diào)用的行為和 pselect() 有些許不同。
? ? ? ? Linux 的 pselect6() 系統(tǒng)調(diào)用修改 timeout 參數(shù),然而 glibc 通過本地緩存 timeout 值隱藏了該行為。因此,glibc? pselect6() 沒有修改 timeout 參數(shù),這也符合 POSIX.1-2001 要求。
????????pselect6() 系統(tǒng)調(diào)用的最后一個參數(shù)不是 sigset_t * 指針類型,而是如下格式:
struct {
const kernel_sigset_t *ss; /* Pointer to signal set */
size_t ss_len; /* Size (in bytes) of object
pointed to by 'ss' */
};
? ? ? ? 這使得系統(tǒng)調(diào)用可以獲取信號集指針及其大小,并考慮到大多數(shù)系統(tǒng)支持最大 6 個系統(tǒng)調(diào)用參數(shù)這個事實。關(guān)于信號處理的差異之處,可以參考 sigprocmask 的討論。
? ? ? ? glibc 歷史細(xì)節(jié)
? ? ? ? gblic 2.0 提供了 pselect() 的錯誤版本,它并沒有 sigmask 參數(shù)。
? ? ? ? glibc 2.1 到 2.2.1,為了獲得 <sys/select.h> 中的 pselect() 聲明,必須定義 _GNU_SOURCE 宏。
9.BUGS
? ? ? ? POSIX 允許實現(xiàn)通過 FD_SETSIZE 來定義文件描述符集中文件描述符的上限,Linux 內(nèi)核并沒有限制,但是 glibc 實現(xiàn)將 fd_set 定為固定長度并將 FD_SETSIZE 設(shè)置為 1024,F(xiàn)D_*() 宏根據(jù)這個限制操作。為了能夠監(jiān)測多余 1023 個文件描述符,可以使用 poll() 或者 epoll。
? ? ? ? fd_set 參數(shù)的輸入輸出屬性是一個錯誤的設(shè)計,已經(jīng)在 poll() 和 epoll() 改正過來。
? ? ? ? 根據(jù) POSIX 要求,select() 應(yīng)該檢查所有集合中的文件描述符不能超過 nfds - 1,但是,當(dāng)前實現(xiàn)會忽略掉那些文件描述符值大于當(dāng)前進(jìn)程打開的最大文件描述符值。根據(jù) POSIX 要求,這些文件描述符會導(dǎo)致 EBADF 錯誤。
? ? ? ? 從 glibc 2.1 開始,glibc 使用 sigprocmask() 和 select() 實現(xiàn)了 pselect() 模擬,這個實現(xiàn)卻遺留了 pselect() 解決的數(shù)據(jù)競爭問題?,F(xiàn)在版本的 glibc 通常使用內(nèi)核提供的不受數(shù)據(jù)競爭影響的 pselect() 系統(tǒng)調(diào)用。
? ? ? ? Linux 上,select()可能報告 socket 文件描述符讀就緒,但是后續(xù)的讀卻會阻塞,這個常發(fā)生在數(shù)據(jù)已達(dá)到但是數(shù)據(jù)的校驗和不對,數(shù)據(jù)被丟棄。當(dāng)然,也可能是誤報。所以使用 O_NONBLOCK 的 sockets 更安全些。
? ? ? ? Linux 上的 select() 會在被信號打斷的情況下更新 timeout 值,POSIX.1 并不允許這樣做。Linux 的 pselect() 是同樣的行為,但是 glibc 隱藏了這種行為。?文章來源:http://www.zghlxwxcb.cn/news/detail-700239.html
10.代碼實例
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
int
main(void)
{
int retval;
fd_set rfds;
struct timeval tv;
/* Watch stdin (fd 0) to see when it has input. */
FD_ZERO(&rfds);
FD_SET(0, &rfds);
/* Wait up to five seconds. */
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv);
/* Don't rely on the value of tv now! */
if (retval == -1)
perror("select()");
else if (retval)
printf("Data is available now.\n");
/* FD_ISSET(0, &rfds) will be true. */
else
printf("No data within five seconds.\n");
exit(EXIT_SUCCESS);
}
下一篇 【計算機網(wǎng)絡(luò)】網(wǎng)絡(luò)編程接口 Socket API 解讀(3)???????文章來源地址http://www.zghlxwxcb.cn/news/detail-700239.html
到了這里,關(guān)于【計算機網(wǎng)絡(luò)】網(wǎng)絡(luò)編程接口 Socket API 解讀(2)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!