网络编程:select
2022/3/20 22:28:52
本文主要是介绍网络编程: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个
- 主动轮询效率很低
这篇关于网络编程:select的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-22怎么实现ansible playbook 备份代码中命名包含时间戳功能?-icode9专业技术文章分享
- 2024-11-22ansible 的archive 参数是什么意思?-icode9专业技术文章分享
- 2024-11-22ansible 中怎么只用archive 排除某个目录?-icode9专业技术文章分享
- 2024-11-22exclude_path参数是什么作用?-icode9专业技术文章分享
- 2024-11-22微信开放平台第三方平台什么时候调用数据预拉取和数据周期性更新接口?-icode9专业技术文章分享
- 2024-11-22uniapp 实现聊天消息会话的列表功能怎么实现?-icode9专业技术文章分享
- 2024-11-22在Mac系统上将图片中的文字提取出来有哪些方法?-icode9专业技术文章分享
- 2024-11-22excel 表格中怎么固定一行显示不滚动?-icode9专业技术文章分享
- 2024-11-22怎么将 -rwxr-xr-x 修改为 drwxr-xr-x?-icode9专业技术文章分享
- 2024-11-22在Excel中怎么将小数向上取整到最接近的整数?-icode9专业技术文章分享