网络编程:select
原理:参考:https://my.oschina.net/fileoptions/blog/911091
select中内核函数有哪些
源码实现:
#undef __NFDBITS
#define __NFDBITS (8 * sizeof(unsigned long))
#undef __FD_SETSIZE
#define __FD_SETSIZE 1024
#undef __FDSET_LONGS
#define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS)
typedef struct {
unsigned longfds_bits [__FDSET_LONGS]; //1024个bit。可以看到可以支持1024个描述符
} __kernel_fd_set;
//系统调用(内核态)
//参数为 maxfd, r_fds, w_fds, e_fds, timeout。
asmlinkage long sys_select(int n, fd_set __user *inp, fd_set __user *outp, fd_set __user *exp, struct timeval __user *tvp)
{
s64 timeout = -1;
struct timeval tv;
int ret;
//将超时时间换成jiffies
if (tvp) {
if (copy_from_user(&tv, tvp, sizeof(tv))) //将用户态参数拷贝到内核态
return -EFAULT;
if (tv.tv_sec < 0 || tv.tv_usec < 0)
return -EINVAL;
/* Cast to u64 to make GCC stop complaining */
if ((u64)tv.tv_sec >= (u64)MAX_INT64_SECONDS)
timeout = -1; /* infinite */
else {
timeout = ROUND_UP(tv.tv_usec, USEC_PER_SEC/HZ);
timeout += tv.tv_sec * HZ;
}
}
// (***) 调用 core_sys_select
ret = core_sys_select(n, inp, outp, exp, &timeout);
//将剩余时间拷贝回用户空间进程
if (tvp) {
struct timeval rtv;
if (current->personality & STICKY_TIMEOUTS) //判断当前环境是否支持修改超时时间(不确定)
goto sticky;
rtv.tv_usec = jiffies_to_usecs(do_div((*(u64*)&timeout), HZ));
rtv.tv_sec = timeout;
if (timeval_compare(&rtv, &tv) >= 0)
rtv = tv;
if (copy_to_user(tvp, &rtv, sizeof(rtv))) {
sticky:
/*
* 如果应用程序将timeval值放在只读存储中,
* 我们不希望在成功完成select后引发错误(修改timeval)
* 但是,因为没修改timeval,所以我们不能重启这个系统调用。
*/
if (ret == -ERESTARTNOHAND)
ret = -EINTR;
}
}
return ret;
}
//主要的工作在这个函数中完成
staticint core_sys_select(int n, fd_set __user *inp, fd_set __user *outp, fd_set __user *exp, s64 *timeout)
{
fd_set_bits fds;
/* fd_set_bits 结构如下:
typedef struct {
unsigned long *in, *out, *ex;
unsigned long *res_in, *res_out, *res_ex;
} fd_set_bits;
这个结构体中定义的全是指针,这些指针都是用来指向描述符集合的。
*/
void *bits;
int ret, max_fds;
unsigned int size;
struct fdtable *fdt;
/* Allocate small arguments on the stack to save memory and be faster 先尝试使用栈(因为栈省内存且快速)*/
long stack_fds[SELECT_STACK_ALLOC/sizeof(long)]; // SELECT_STACK_ALLOC=256
ret = -EINVAL;
if (n < 0)
goto out_nofds;
/* max_fds can increase, so grab it once to avoid race */
rcu_read_lock(); //rcu锁
fdt = files_fdtable(current->files); //读取文件描述符表
/* struct fdtable 结构如下:
struct fdtable {
unsigned int max_fds;
struct file **fd;
...
};
*/
max_fds = fdt->max_fds; //从files结构中获取最大值(当前进程能够处理的最大文件数目)
rcu_read_unlock();
if (n > max_fds)// 如果传入的n大于当前进程最大的文件描述符,给予修正
n = max_fds;
/* 我们需要使用6倍于最大描述符的描述符个数,
* 分别是in/out/exception(参见fd_set_bits结构体),
* 并且每份有一个输入和一个输出(用于结果返回) */
size = FDS_BYTES(n);// 以一个文件描述符占一bit来计算,传递进来的这些fd_set需要用掉多少个字
bits = stack_fds;
if (size > sizeof(stack_fds) / 6) { // 除以6,因为每个文件描述符需要6个bitmaps上的位。
//栈不能满足,先前的尝试失败,只能使用kmalloc方式
/* Not enough space in on-stack array; must use kmalloc */
ret = -ENOMEM;
bits = kmalloc(6 * size, GFP_KERNEL);
if (!bits)
goto out_nofds;
}
//设置fds
fds.in = bits;
fds.out = bits + size;
fds.ex = bits + 2*size;
fds.res_in = bits + 3*size;
fds.res_out = bits + 4*size;
fds.res_ex = bits + 5*size;
// get_fd_set仅仅调用copy_from_user从用户空间拷贝了fd_se
if ((ret = get_fd_set(n, inp, fds.in)) ||
(ret = get_fd_set(n, outp, fds.out)) ||
(ret = get_fd_set(n, exp, fds.ex)))
goto out;
// 对这些存放返回状态的字段清0
zero_fd_set(n, fds.res_in);
zero_fd_set(n, fds.res_out);
zero_fd_set(n, fds.res_ex);
// 执行do_select,完成监控功能
ret = do_select(n, &fds, timeout);
if (ret < 0) // 有错误
goto out;
if (!ret) { // 超时返回,无设备就绪
ret = -ERESTARTNOHAND;
if (signal_pending(current))
goto out;
ret = 0;
}
if (set_fd_set(n, inp, fds.res_in) ||
set_fd_set(n, outp, fds.res_out) ||
set_fd_set(n, exp, fds.res_ex))
ret = -EFAULT;
out:
if (bits != stack_fds)
kfree(bits);
out_nofds:
return ret;
}
#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
#define POLLEX_SET (POLLPRI)
int do_select(int n, fd_set_bits *fds, s64 *timeout)
{
struct poll_wqueues table;
/*
struct poll_wqueues {
poll_table pt;
struct poll_table_page *table;
struct task_struct *polling_task; //保存当前调用select的用户进程struct task_struct结构体
int triggered; // 当前用户进程被唤醒后置成1,以免该进程接着进睡眠
int error; // 错误码
int inline_index; // 数组inline_entries的引用下标
struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};
*/
poll_table *wait;
int retval, i;
rcu_read_lock();
//根据已经设置好的fd位图检查用户打开的fd, 要求对应fd必须打开, 并且返回最大的fd。
retval = max_select_fd(n, fds);
rcu_read_unlock();
if (retval < 0)
return retval;
n = retval;
/* 一些重要的初始化:
poll_wqueues.poll_table.qproc函数指针初始化,
该函数是驱动程序中poll函数(fop->poll)实现中必须要调用的poll_wait()中使用的函数。 */
poll_initwait(&table);
wait = &table.pt;
if (!*timeout)
wait = NULL; // 用户设置了超时时间为0
retval = 0;
for (;;) {
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
long __timeout;
set_current_state(TASK_INTERRUPTIBLE);
inp = fds->in; outp = fds->out; exp = fds->ex;
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
// 所有n个fd的循环
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;
// 先取出当前循环周期中的32(设long占32位)个文件描述符对应的bitmaps
in = *inp++; out = *outp++; ex = *exp++;
all_bits = in | out | ex;// 组合一下,有的fd可能只监测读,或者写,或者err,或者同时都监测
if (all_bits == 0) {
i += __NFDBITS; //如果这个字没有待查找的描述符, 跳过这个长字(32位,__NFDBITS=32),取下一个32个fd的循环中
continue;
}
// 本次32个fd的循环中有需要监测的状态存在
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
int fput_needed;
if (i >= n)
break;
if (!(bit & all_bits)) // bit每次循环后左移一位的作用在这里,用来跳过没有状态监测的fd
continue;
file = fget_light(i, &fput_needed);//得到file结构指针,并增加引用计数字段f_count
if (file) {// 如果file存在(这个文件描述符对应的文件确实打开了)
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
if (f_op && f_op->poll) //这个文件对应的驱动程序提供了poll函数(fop->poll)。
mask = (*f_op->poll)(file, retval ? NULL : wait);//调用驱动程序中的poll函数。
/* 调用驱动程序中的poll函数,以evdev驱动中的evdev_poll()为例
* 该函数会调用函数poll_wait(file, &evdev->wait, wait),
* 继续调用__pollwait()回调来分配一个poll_table_entry结构体,
* 该结构体有一个内嵌的等待队列项,
* 设置好wake时调用的回调函数后将其添加到驱动程序中的等待队列头中。 */
fput_light(file, fput_needed); // 释放file结构指针,实际就是减小他的一个引用计数字段f_count。
//记录结果。poll函数返回的mask是设备的状态掩码。
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit; //如果是这个描述符可读, 将这个位置位
retval++; //返回描述符个数加1
}
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit;
retval++;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit;
retval++;
}
}
/*
* cond_resched()将判断是否有进程需要抢占当前进程,
* 如果是将立即发生调度,这只是为了增加强占点。
* (给其他紧急进程一个机会去执行,增加了实时性)
* 在支持抢占式调度的内核中(定义了CONFIG_PREEMPT),
* cond_resched是空操作。
*/
cond_resched();
}
//返回结果
if (res_in)
*rinp = res_in;
if (res_out)
*routp = res_out;
if (res_ex)
*rexp = res_ex;
}
wait = NULL;
if (retval || !*timeout || signal_pending(current)) // signal_pending(current)检查当前进程是否有信号要处理
break;
if(table.error) {
retval = table.error;
break;
}
if (*timeout < 0) {
/* Wait indefinitely 无限期等待*/
__timeout = MAX_SCHEDULE_TIMEOUT;
} elseif (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT - 1)) {
/* Wait for longer than MAX_SCHEDULE_TIMEOUT. Do it in a loop */
__timeout = MAX_SCHEDULE_TIMEOUT - 1;
*timeout -= __timeout;
} else {
__timeout = *timeout;
*timeout = 0;
}
/* schedule_timeout 用来让出CPU;
* 在指定的时间用完以后或者其它事件到达并唤醒进程(比如接收了一个信号量)时,
* 该进程才可以继续运行 */
__timeout = schedule_timeout(__timeout);
if (*timeout >= 0)
*timeout += __timeout;
}
__set_current_state(TASK_RUNNING);
poll_freewait(&table);
return retval;
}
源码中比较重要的结构体有四个:
struct poll_wqueues
、struct poll_table_page
、struct poll_table_entry
、struct poll_table_struct
。
每一个调用select()系统调用的应用进程都会存在一个struct poll_wqueues结构体,用来统一辅佐实现这个进程中所有待监测的fd的轮询工作,后面所有的工作和都这个结构体有关,所以它非常重要。
struct poll_wqueues {
poll_table pt;
struct poll_table_page *table;
struct task_struct *polling_task; //保存当前调用select的用户进程struct task_struct结构体
int triggered; // 当前用户进程被唤醒后置成1,以免该进程接着进睡眠
int error; // 错误码
int inline_index; // 数组inline_entries的引用下标
struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};
实际上结构体poll_wqueues内嵌的poll_table_entry数组inline_entries[] 的大小是有限的,如果空间不够用,后续会动态申请物理内存页以链表的形式挂载poll_wqueues.table上统一管理。接下来的两个结构体就和这项内容密切相关:
struct poll_table_page { // 申请的物理页都会将起始地址强制转换成该结构体指针
struct poll_table_page *next; // 指向下一个申请的物理页
struct poll_table_entry *entry; // 指向entries[]中首个待分配(空的) poll_table_entry地址
struct poll_table_entry entries[0]; // 该page页后面剩余的空间都是待分配的poll_table_entry结构体
};
对每一个fd调用fop->poll() => poll_wait() => __pollwait()都会先从poll_wqueues.inline_entries[]中分配一个poll_table_entry结构体,直到该数组用完才会分配物理页挂在链表指针poll_wqueues.table上然后才会分配一个poll_table_entry结构体(poll_get_entry函数)。
poll_table_entry具体用处:函数__pollwait声明如下
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p);
该函数调用时需要3个参数,第一个是特定fd对应的file结构体指针,第二个就是特定fd对应的硬件驱动程序中的等待队列头指针,第3个是调用select()的应用进程中poll_wqueues结构体的poll_table项(该进程监测的所有fd调用fop->poll函数都用这一个poll_table结构体)。
struct poll_table_entry {
struct file *filp; // 指向特定fd对应的file结构体;
unsigned long key; // 等待特定fd对应硬件设备的事件掩码,如POLLIN、 POLLOUT、POLLERR;
wait_queue_t wait; // 代表调用select()的应用进程,等待在fd对应设备的特定事件 (读或者写)的等待队列头上,的等待队列项;
wait_queue_head_t *wait_address; // 设备驱动程序中特定事件的等待队列头(该fd执行fop->poll,需要等待时在哪等,所以叫等待地址);
};
API
Select函数的声明:
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
- maxfd 表示的是待测试的描述符基数,它的值是待测试的最大描述符加 1。比如:描述字集合{0,1,4},对应的 maxfd 是 5,而不是 4
很多系统是用一个整型数组来表示一个描述字集合的,一个 32 位的整型数可以表示 32 个描述字,例如第一个整型数表示 0-31 描述字,第二个整型数可以表示 32-63 描述字,以此类推。
- 紧接着的是三个描述符集合,分别是读描述符集合 readset、写描述符集合 writeset 和异常描述符集合 exceptset,这三个分别通知内核,在哪些描述符上检测数据可以读,可以写和有异常发生。
void FD_ZERO(fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
int FD_ISSET(int fd, fd_set *fdset);
- FD_ZERO 用来将这个向量的所有元素都设置成 0;
- FD_SET 用来把对应套接字 fd 的元素,a[fd]设置成 1;
- FD_CLR 用来把对应套接字 fd 的元素,a[fd]设置成 0;
- FD_ISSET 对这个向量进行检测,判断出对应套接字的元素 a[fd]是 0 还是 1。
其中 0 代表不需要处理,1 代表需要处理。
- 最后一个参数是 timeval 结构体时间:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
这个参数设置不同的值,会有不同的可能:
- 第一个可能是设置成空 (NULL),表示如果没有 I/O 事件发生,则 select 一直等待下去。
- 第二个可能是设置一个非零的值,这个表示等待固定的一段时间后从 select 阻塞调用中返回
- 第三个可能是将 tv_sec 和 tv_usec 都设置成 0,表示根本不等待,检测完毕立即返回。这种情况使用得比较少。
实践
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 1024
#define SERV_PORT 43211
tcp_client(char *address, int port)
{
int socket_fd;
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, address, server_addr.sin_addr);
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int connfd_rt = connect(socket_fd, (struct sockaddr *)&client_addr, client_len);
if(connfd_rt < 0)
{
perror("connect failed");
return -1;
}
return socket_fd;
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
perror("usage:select <IPaddress>");
return -1;
}
int socket_fd;
socket_fd = tcp_client(argv[1], SERV_PORT);
char recv_line[MAXLINE], send_line[MAXLINE];
int n;
fd_set readmask;
fd_set allreads;
FD_ZERO(&allreads);
FD_SET(0, &allreads);
FD_SET(socket_fd, &allreads);
for(;;)
{
readmask = allreads;
int rc = select(socket_fd+1, &readmask, NULL, NULL, NULL);
if(rc <= 0)
{
perror("select failed");
return -1;
}
if(FD_ISSET(socket_fd, &readmask))
{
n = read(socket_fd, recv_line, MAXLINE);
if(n < 0)
{
perror("read error");
return -1;
}
else if(n == 0)
{
printf("server terminated\n");
return 0;
}
recv_line[n] = 0;
fputs(recv_line, stdout);
fputs("\n", stdout);
}
if(FD_ISSET(STDIN_FILENO, &readmask))
{
if(fgets(send_line, MAXLINE, stdin) != NULL)
{
int i = strlen(send_line);
if(send_line[i - 1] == '\n')
{
send_line[i - 1] = 0;
}
printf("now sending %s\n",send_line);
ssize_t rt = write(socket_fd, send_line, strlen(send_line));
if(rt < 0)
{
perror("write failed");
return -1;
}
printf("send bytes: %zu \n",rt);
}
}
}
}
通过 FD_ZERO 初始化了一个描述符集合,这个描述符读集合是空的:
分别使用 FD_SET 将描述符 0,即标准输入,以及连接套接字描述符 3 设置为待检测:
通过 select 来检测套接字描述字有数据可读,或者标准输入有数据可读。比如,当用户通过标准输入使得标准输入描述符可读时,返回的 readmask 的值为:
这个时候 select 调用返回,可以使用 FD_ISSET 来判断哪个描述符准备好可读了。如上图所示,这个时候是标准输入可读
select 调用每次完成测试之后,内核都会修改描述符集合,通过修改完的描述符集合来和应用程序交互,应用程序使用 FD_ISSET 来对每个描述符进行判断
使用 socket_fd+1 来表示待测试的描述符基数。切记需要 +1。套接字描述符就绪条件
小结
- 描述符基数是当前最大描述符 +1;
- 每次 select 调用完成之后,记得要重置待测试集合。
select的缺点:
- 内核需要将消息传递到用户空间,都需要内核拷贝动作。需要维护一个用来存放大量fd的数据结构,使得用户空间和内核空间在传递该结构时复制开销大。
- 每次调用select,都需把fd集合从用户态拷贝到内核态,fd很多时开销就很大
- 同时每次调用select都需在内核遍历传递进来的所有fd,fd很多时开销就很大
- select支持的文件描述符数量太小了,默认最大支持1024个
- 主动轮询效率很低