在Linux環(huán)境下select函數(shù)的初體驗(yàn)
select介紹
在linux中, 主要的 IO復(fù)用方式中, 有epoll, poll 和select, 這次先來(lái)學(xué)習(xí)下select.
select 能夠同時(shí)監(jiān)視多個(gè)文件描述符的變法, 也支持超時(shí)返回.
先來(lái)看下select函數(shù)的定義
- /* /usr/include/sys/select.h */
- extern int select (int __nfds, // 最大文件描述符+1
- fd_set *__restrict __readfds, // 讀狀態(tài)文件集
- fd_set *__restrict __writefds, // 寫狀態(tài)文件集
- fd_set *__restrict __exceptfds, // 異常狀態(tài)文件集
- struct timeval *__restrict __timeout); // 超時(shí)時(shí)間
如上圖函數(shù)聲明所示, 不管我們關(guān)注什么狀態(tài), 我們都應(yīng)該把同一類狀態(tài)的文件描述符存到同一個(gè)fd_set集合,以便select能夠相應(yīng)的位置打上標(biāo)簽, 以便后續(xù)我們來(lái)判斷該文件描述符是否已經(jīng)準(zhǔn)備好
這些傳遞給select函數(shù)的參數(shù), 將告訴內(nèi)核:
- 我們需要監(jiān)聽(tīng)的文件描述符
- 對(duì)于每個(gè)文件描述符, 我們所關(guān)心的狀態(tài) (讀/寫/異常)
- 我們要等待多長(zhǎng)時(shí)間 (無(wú)限長(zhǎng)/超時(shí)返回)
而內(nèi)核也會(huì)通過(guò)select的返回, 告知我們一些信息:
- 已經(jīng)準(zhǔn)備好的文件描述符個(gè)數(shù)
- 那三種狀態(tài)分別是哪些文件描述符
我們可以通過(guò)以下方式將關(guān)注的文件描述符加入相應(yīng)的文件集:
- int socket_test;
- socket_test = socket(...); //創(chuàng)建socket文件描述符
- connent(socket_test,..); //連接服務(wù)端
- FD_SET(socket_test, &rdfds); //加入讀狀態(tài)文件集
- FD_SET(socket_test, &wdfds); //加入寫狀態(tài)文件集
- ....
select原理
select函數(shù)執(zhí)行順序是: SYSCALL_DEFINE5 (sys_select) -> core_sys_select -> do_select
我們都知道, select 支持監(jiān)聽(tīng)三個(gè)文件集: 讀文件集, 寫文件集, 異常文件集;
在我們調(diào)用FD_SET(socket_test, &rdfds)時(shí), 實(shí)際上執(zhí)行的操作是: 在rdfds成員數(shù)組中, 將__FDELT (d)位置的值 設(shè)成 __FDMASK (d), 直接說(shuō)會(huì)有點(diǎn)疑惑, 先看下相關(guān)的函數(shù),宏定義是怎樣定義的吧:
- /* 取自: /usr/include/sys/select.h */
- #define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp)
- typedef long int __fd_mask;
- /* 取自: /usr/include/bits/select.h */
- #define __NFDBITS (8 * (int) sizeof (__fd_mask))
- #define __FDELT(d) ((d) / __NFDBITS)
- #define __FDMASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS))
- #define __FD_SET(d, set) (__FDS_BITS (set)[__FDELT (d)] |= __FDMASK (d))
- typedef struct
- {
- /* XPG4.2 requires this member name. Otherwise avoid the name
- from the global namespace. */
- #ifdef __USE_XOPEN
- __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
- # define __FDS_BITS(set) ((set)->fds_bits)
- #else
- __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
- # define __FDS_BITS(set) ((set)->__fds_bits)
- #endif
- } fd_set;
- /* /usr/include/linux/posix_types.h */
- #define __FD_SETSIZE 1024
舉個(gè)栗子, 假設(shè) fd=3, 當(dāng)我們執(zhí)行FD_SET(fd, &rdfds)時(shí):
- 算出 __FDELT(d) 和 __FDMASK(d)的值, 通過(guò)上面的宏定義, 可以分別得出結(jié)果: 3/(8*8), 1<<3%(8*8), 也就是0 和 二進(jìn)制的 0000 0100
- 然后分別將值存入 rdfds.__fds_bits第0個(gè)位置, 值為十進(jìn)制的8
- 我們可以將__fds_bit的每個(gè)索引看成是一個(gè)聚合的過(guò)程, 每個(gè)值8字節(jié), 也就是有64位, 可以存64個(gè)fd, 在我的系統(tǒng)上, 算出數(shù)組的長(zhǎng)度是__FD_SETSIZE / __NFDBITS = 1024/8=128個(gè), 也就是大概能容納 128*64=8192(如果理解錯(cuò)誤請(qǐng)指出)
經(jīng)過(guò)上面的運(yùn)算, 我們將需要關(guān)注的文件描述關(guān)聯(lián)到 rdfds文件集了, 對(duì)于寫文件集, 異常文件集都是同樣的運(yùn)算, 等這些步驟都進(jìn)行完了, 接下來(lái)就是進(jìn)入core_sys_select函數(shù)了:
- 執(zhí)行到 core_sys_select 時(shí), 定義一個(gè)fd_set_bits結(jié)構(gòu)體: fds.
- 分別為fds的成員(in, out, ex, res_in, res_out, res_ex)申請(qǐng)內(nèi)存
- 將我們傳給select的 rdfds, wrfds, exfds分別賦值給 in, out, ex, 這樣fds就能記錄三個(gè)集合的結(jié)果了
- 初始化那個(gè)三個(gè)成員之后, 將執(zhí)行do_select(n, &fds, end_time)
- 在do_select中, 函數(shù)將進(jìn)入死循環(huán),其中還有兩個(gè)循環(huán), 分別是針對(duì) "最大文件描述符數(shù)" 和 fd_set_bits數(shù)組中單個(gè)值位數(shù). 從上面我們已經(jīng)知道, 在fd_set_bits每個(gè)值都代表所關(guān)注的文件描述符, 每個(gè)值是__NFDBITS(8 *8字節(jié))大小,也就是64位, 所以在上面循環(huán)內(nèi), 還要再循環(huán)64次
- 看到這里其實(shí)大家都應(yīng)該有個(gè)底了, 為什么要循環(huán)那么多次, 因?yàn)槲覀冃枰ㄟ^(guò)每個(gè)文件描述符對(duì)應(yīng)的file_operations結(jié)構(gòu)體的接口f_op->poll來(lái)得知是否已經(jīng)準(zhǔn)備好了
簡(jiǎn)單介紹 file_operations
我們都知道,當(dāng)我們打開(kāi)一些設(shè)備或者文件時(shí), 總是返回一個(gè)文件描述符, 其實(shí)通過(guò)這個(gè)文件描述符, 我們通過(guò)fget_light 來(lái)獲得對(duì)應(yīng)的file結(jié)構(gòu)體, 為什么還要反查這個(gè)file, 因?yàn)橥ㄟ^(guò)這個(gè)file結(jié)構(gòu)體可以得到: file_operations結(jié)構(gòu)體
file_operations結(jié)構(gòu)體: 用來(lái)存儲(chǔ)驅(qū)動(dòng)內(nèi)核模塊提供的對(duì) 設(shè)備進(jìn)行各種操作的函數(shù)的指針。該結(jié)構(gòu)體的每個(gè)域都對(duì)應(yīng)著驅(qū)動(dòng)內(nèi)核模塊用來(lái)處理某個(gè)被請(qǐng)求的 事務(wù)的函數(shù)的地址。
- /* linux-2.6.32/include/linux/fs.h */
- struct file_operations {
- ...
- unsigned int (*poll) (struct file *, struct poll_table_struct *); // select通過(guò)這個(gè)來(lái)獲取狀態(tài)
- ...(其他接口忽略)
do_select 循環(huán)體源碼:
- /* select.c/do_select() */
- for (;;) {
- unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
- inp = fds->in; outp = fds->out; exp = fds->ex;
- rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
- for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
- unsigned long in, out, ex, all_bits, bit = 1, mask, j;
- unsigned long res_in = 0, res_out = 0, res_ex = 0;
- const struct file_operations *f_op = NULL;
- struct file *file = NULL;
- in = *inp++; out = *outp++; ex = *exp++;
- all_bits = in | out | ex;
- if (all_bits == 0) {
- i += __NFDBITS;
- continue;
- }
- for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) { // 遍歷64位
- int fput_needed;
- if (i >= n)
- break;
- if (!(bit & all_bits))
- continue;
- //在當(dāng)前進(jìn)程的struct files_struct中根據(jù)所謂的用戶空間文件描述符fd來(lái)獲取文件描述符
- file = fget_light(i, &fput_needed);
- if (file) {
- f_op = file->f_op; // file_operations結(jié)構(gòu)體
- mask = DEFAULT_POLLMASK;
- if (f_op && f_op->poll) {
- wait_key_set(wait, in, out, bit);
- mask = (*f_op->poll)(file, wait);
- }
- fput_light(file, fput_needed);
- if ((mask & POLLIN_SET) && (in & bit)) { //判斷讀狀態(tài)
- res_in |= bit;
- retval++;
- wait = NULL;
- }
- if ((mask & POLLOUT_SET) && (out & bit)) { //判斷寫狀態(tài)
- res_out |= bit;
- retval++;
- wait = NULL;
- }
- if ((mask & POLLEX_SET) && (ex & bit)) { //判斷異常狀態(tài)
- res_ex |= bit;
- retval++;
- wait = NULL;
- }
- }
- }
- if (res_in)
- *rinp = res_in;
- if (res_out)
- *routp = res_out;
- if (res_ex)
- *rexp = res_ex;
- cond_resched();
- }
- wait = NULL;
- if (retval || timed_out || signal_pending(current))
- break;
- if (table.error) {
- retval = table.error;
- break;
- }
- /*
- * If this is the first loop and we have a timeout
- * given, then we convert to ktime_t and set the to
- * pointer to the expiry value.
- */
- if (end_time && !to) {
- expire = timespec_to_ktime(*end_time);
- to = &expire;
- }
- if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
- to, slack))
- timed_out = 1;
- }
當(dāng)select經(jīng)歷完上面的流程, 將會(huì)有以下結(jié)果:
- >0: 準(zhǔn)備好的文件描述符個(gè)數(shù)
- 0: 超時(shí)
- -1: 出錯(cuò)或者接收到信號(hào)
那我們接下來(lái)要做的就是,
- 通過(guò)行FD_ISSET()判斷之前綁定的文件fd, 如果為真, 則進(jìn)行相應(yīng)操作
- 因?yàn)閟elect返回后, 之前存好的rdfds, wdfds, exfds都會(huì)被清空, 所以需要用FD_SET()重新加入
select實(shí)戰(zhàn)
上面已經(jīng)學(xué)習(xí)到關(guān)于select的相關(guān)知識(shí), 那么我們應(yīng)該要來(lái)實(shí)戰(zhàn)下:
這次我們需要實(shí)現(xiàn)的目標(biāo)是:
一個(gè)程序, 同時(shí)連接3個(gè)socket_server, 并且將socket_server發(fā)送的消息打印出來(lái)(不需要響應(yīng), 也不需要交互)
程序代碼:
- /* filename: test_select.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/socket.h>
- #include <sys/select.h>
- #include <sys/types.h>
- #include <netinet/in.h>
- #include <unistd.h>
- #include <fcntl.h>
- void main()
- {
- // socket1
- int socketd;
- char buffer[1025];
- struct sockaddr_in seraddr;
- socketd = socket(AF_INET, SOCK_STREAM, 0);
- seraddr.sin_family = AF_INET;
- seraddr.sin_port = htons(9997);
- inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr);
- if (connect(socketd, (struct sockaddr *) &seraddr, sizeof(seraddr))<0)
- {
- printf("socketd1 connect failed\n");
- exit(3);
- }
- // socket2
- int socketd2;
- char buffer2[1025];
- struct sockaddr_in seraddr2;
- socketd2 = socket(AF_INET, SOCK_STREAM, 0);
- seraddr2.sin_family = AF_INET;
- seraddr2.sin_port = htons(9998);
- inet_pton(AF_INET, "127.0.0.1", &seraddr2.sin_addr);
- if (connect(socketd2, (struct sockaddr *) &seraddr2, sizeof(seraddr))<0)
- {
- printf("socketd2 connect failed\n");
- exit(3);
- }
- // scoket3
- int socketd3;
- char buffer3[1025];
- struct sockaddr_in seraddr3;
- socketd3 = socket(AF_INET, SOCK_STREAM, 0);
- seraddr3.sin_family = AF_INET;
- seraddr3.sin_port = htons(9999);
- inet_pton(AF_INET, "127.0.0.1", &seraddr3.sin_addr);
- if (connect(socketd3, (struct sockaddr *) &seraddr3, sizeof(seraddr))<0)
- {
- printf("socketd3 connect failed\n");
- exit(3);
- }
- int maxfdp;
- fd_set fds; // select需要的文件描述符集合
- maxfdp = socketd3 + 1; // select 第一個(gè)形參就是打開(kāi)的最大文件描述符+1
- struct timeval timeout = {3, 0}; // 超時(shí)設(shè)置
- while(1)
- {
- FD_ZERO(&fds); // 初始化文件描述符集合
- FD_SET(socketd, &fds); // 分別添加以上三個(gè)需要監(jiān)聽(tīng)的文件描述符
- FD_SET(socketd2, &fds);
- FD_SET(socketd3, &fds);
- select(maxfdp, &fds, NULL, NULL, &timeout);
- // 通過(guò)FD_ISSET 來(lái)分別判斷 監(jiān)聽(tīng)的文件描述符在fds有沒(méi)有被設(shè)置成1
- if (FD_ISSET(socketd, &fds))
- {
- read(socketd, buffer, 1024);
- printf("1 %s\n",buffer);
- }
- if(FD_ISSET(socketd2, &fds))
- {
- read(socketd2, buffer2, 1024);
- printf("2 %s\n",buffer2);
- }
- if(FD_ISSET(socketd3, &fds))
- {
- read(socketd3, buffer3, 1024);
- printf("3 %s\n",buffer3);
- }
- }
- }
為了快速建立簡(jiǎn)單的測(cè)試服務(wù)端, 所以用python實(shí)現(xiàn)簡(jiǎn)單socket_server:
- # socket1.py
- import socket
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.bind(('localhost', 9997))
- s.listen(2)
- rint 'Socket1 is on ready!'
- client, info = s.accept()
- print info
- while 1:
- message = raw_input('input: ')
- client.send(message)
- s.close()
- -------------------------------
- # socket2.py
- import socket
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.bind(('localhost', 9998))
- s.listen(2)
- rint 'Socket2 is on ready!'
- client, info = s.accept()
- print info
- while 1:
- message = raw_input('input: ')
- client.send(message)
- s.close()
- -------------------------------
- # socket3.py
- import socket
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.bind(('localhost', 9999))
- s.listen(2)
- rint 'Socket3 is on ready!'
- client, info = s.accept()
- print info
- while 1:
- message = raw_input('input: ')
- client.send(message)
- s.close()
分別運(yùn)行 socket1.py, socket2.py, socket3.py將會(huì)看到如下結(jié)果:
- # 運(yùn)行 socket1.py
- [root@iZ23pynfq19Z ~]# python socket1.py
- Socket1 is on ready!
- ----------------------
- # 運(yùn)行 socket1.py
- [root@iZ23pynfq19Z ~]# python socket2.py
- Socket2 is on ready!
- ----------------------
- # 運(yùn)行 socket1.py
- [root@iZ23pynfq19Z ~]# python socket3.py
- Socket3 is on ready!
當(dāng)我們編譯 test_select.c 并運(yùn)行時(shí), 將會(huì)看到三個(gè)服務(wù)端都出現(xiàn)了相應(yīng)的響應(yīng):
- # 運(yùn)行 socket1.py
- [root@iZ23pynfq19Z ~]# python socket1.py
- Socket1 is on ready!
- ('127.0.0.1', 55951) # 客戶端連接的信息, 端口不一定相同
- input:
- ----------------------
- # 運(yùn)行 socket1.py
- [root@iZ23pynfq19Z ~]# python socket2.py
- Socket2 is on ready!
- ('127.0.0.1', 55921)
- input:
- ----------------------
- # 運(yùn)行 socket1.py
- [root@iZ23pynfq19Z ~]# python socket3.py
- Socket3 is on ready!
- ('127.0.0.1', 55933)
- input:
那么我們來(lái)嘗試三個(gè)服務(wù)端分別發(fā)送消息到select程序吧:
- # socket1.py
- [root@iZ23pynfq19Z ~]# python socket1.py
- Socket1 is on ready!
- ('127.0.0.1', 55951) # 客戶端連接的信息, 端口不一定相同
- input: asd
- input: qwe
- input: as
- input:
- ----------------------
- # socket1.py
- [root@iZ23pynfq19Z ~]# python socket2.py
- Socket2 is on ready!
- ('127.0.0.1', 55921)
- input: asd
- input: asd
- input: asd
- input: as
- input: s
- input:
- ----------------------
- # socket1.py
- [root@iZ23pynfq19Z ~]# python socket3.py
- Socket3 is on ready!
- ('127.0.0.1', 55933)
- input: asd
- input: qwe
- input: a
- input:
將看到select程序都能輸出三個(gè)socket_server發(fā)出的消息:
需要注意的是:
- 前面的數(shù)字是socket_server的編號(hào), 因?yàn)閟erver發(fā)送消息的順序是亂的, 所以輸出的編號(hào)也是亂的
- 這次只為驗(yàn)證select, 所以并沒(méi)對(duì)程序的健壯性作較好的設(shè)計(jì), 所以如果服務(wù)端/客戶端刷屏了, 直接ctrl-c終止吧
經(jīng)過(guò)上述的實(shí)驗(yàn), 我們應(yīng)該能夠簡(jiǎn)單的了解select的用法和效果, 通過(guò)select實(shí)現(xiàn)IO多路復(fù)用, 可以讓我們一定程度上避免多線程/多進(jìn)程的繁瑣, 在我們?nèi)粘9ぷ魃? 有必要的話嘗試這種方式也不失一種偷懶的方法.