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,需要等待时在哪等,所以叫等待地址); };
Select函数的声明:
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout); 返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
很多系统是用一个整型数组来表示一个描述字集合的,一个 32 位的整型数可以表示 32 个描述字,例如第一个整型数表示 0-31 描述字,第二个整型数可以表示 32-63 描述字,以此类推。
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 代表需要处理。
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。套接字描述符就绪条件