
在Linux中,网络编程经常会涉及到fd_set结构体的使用。该结构体是操作系统提供的一种数据类型,主要用于描述一组文件描述符,可以在同一个数组中同时存放多个文件描述符。在本文中,我们将。
一、fd_set结构体的定义
fd_set结构体是Linux系统中的一个数据类型,定义在头文件中:
typedef struct fd_set {
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
} fd_set;
其中,__fd_mask和__FD_SETSIZE是系统定义的宏,__NFDBITS是一个数值常量。fds_bits数组是一个位向量,存放的是文件描述符的状态。
二、函数分析
1、FD_ZERO(fd_set *set)函数
FD_ZERO函数用于清空fd_set结构体的所有元素,即将每个元素都设置为0。函数原型如下:
int FD_ZERO(fd_set* set);
参数set是一个指向fd_set结构体的指针,返回值为0表示清空成功,-1表示失败。
2、FD_SET(int fd, fd_set *set)函数
FD_SET函数用于将文件描述符fd添加到fd_set结构体set中。函数原型如下:
void FD_SET(int fd, fd_set* set);
参数fd是待添加的文件描述符,set是指向保存文件描述符的fd_set结构体的指针。FD_SET函数将fd文件描述符添加到set中,以便进一步使用。如果fd失败或者已经存在于set中,将不做任何改变。
3、FD_CLR(int fd, fd_set *set)函数
FD_CLR函数用于将文件描述符fd从fd_set结构体set中删除。函数原型如下:
void FD_CLR(int fd, fd_set* set);
参数fd是待删除的文件描述符,set是指向保存文件描述符的fd_set结构体的指针。FD_CLR函数将fd文件描述符从set中删除,以便进一步使用。如果fd不在fd_set结构体中,FD_CLR函数将不做任何改变。
4、FD_ISSET(int fd, fd_set *set)函数
FD_ISSET函数用于判断文件描述符fd是否在fd_set结构体set中。函数原型如下:
int FD_ISSET(int fd, fd_set* set);
参数fd是待判断的文件描述符,set是指向保存了文件描述符的fd_set结构体的指针。FD_ISSET函数将会检测fd是否在set中,如果存在则返回1,不存在则返回0。
三、使用示例
为了更好地展示fd_set结构体的用法,我们在此列举一个简单的程序示例:
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 1024
#define LISTENQ 5
int mn(int argc, char **argv) {
int listenfd, connfd;
struct sockaddr_in servaddr, cliaddr;
socklen_t clilen;
char buf[MAXLINE];
int i, maxi, maxfd, sockfd;
int nready, client[FD_SETSIZE];
fd_set rset, allset;
/* 创建监听套接字 */
listenfd = socket(AF_INET, SOCK_STREAM, 0);
/* 设置套接字地址重复利用 */
int optval = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, sizeof(int));
/* 绑定套接字地址 */
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9938);
bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
/* 监听 */
listen(listenfd, LISTENQ);
maxfd = listenfd;
maxi = -1;
for (i = 0; i
client[i] = -1;
}
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
while (1) {
rset = allset;
nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
if (FD_ISSET(listenfd, &rset)) {
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
for (i = 0; i
if (client[i]
client[i] = connfd;
break;
}
}
if (i == FD_SETSIZE) {
fprintf(stderr, “too many clients”);
exit(1);
}
FD_SET(connfd, &allset);
if (connfd > maxfd) {
maxfd = connfd;
}
if (i > maxi) {
maxi = i;
}
if (–nready
continue;
}
}
for (i = 0; i
if ((sockfd = client[i])
continue;
}
if (FD_ISSET(sockfd, &rset)) {
if ((n = read(sockfd, buf, MAXLINE)) == 0) {
/* connection closed by client */
close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else {
write(sockfd, buf, n);
}
if (–nready
break;
}
}
}
}
}
该程序实现了一个简单的echo服务器,其具体实现如下:
① 创建一个监听套接字。
② 绑定套接字地址。
③ 监听套接字。
④ 初始化一个fd_set结构体,将监听套接字添加到其中。
⑤ 循环调用select函数,等待连接请求或客户端数据,并进行相应的处理。
相关问题拓展阅读:
- windows select模型 和linux的区别
- 关于Linux下的select/epoll
windows select模型 和linux的区别
windows select模型
在widows下提供了众多非阻塞的I/O模型,如select、WSAAsyncSelect、WSAEventSelect、overlapped、completion port,其中IO completion port(IOCP)提供了较好的伸缩性,在windows应用比较广泛
说明:而select模型主要是解决在单一线程模式下只能处理一个套接字的问题,这样可以避免线程膨胀问题,但是。。。下面看完原型再讲起不足之处
select模型:winsock库主要有两个版本,这里主要以winsock2版本为说明,select模型
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,const struct timeval* timeout);
其实主要是两者采用的标准一致,所以接口基本跟linux一致,不过nfds在windows其实是没有意义的,主要是为了兼容其他版本
处理过程:假设以read为例,在这里windows主要是先将套接字s添加到readfds中,然后等待select函数返回,在select函数里面会移除没有未决的
I/O操作的套接字句柄,即已经处理过的IO套接字句柄,然后看s是信销前否认仍然还是readfs中,在就说明s可读了,但是这里面可读,不滑清一定有数据
有几种情况都会引发:数据可读、连接关闭/重启/中断、监听套接字被调用,此时还有连接未决,accept函数接受新的套接字成功
不足:其实添加到fd_set套接字数量是有限制的,winsock2.h定义的64,自定义也不超过1024,因为值太大,会对服务器的性能有影响,更高的就是可伸缩的IOCP
linux select模型
其实原理跟windows是差不多的,只是处理过程在底层上有点区别
模型:int select(int maxfd,fd_set*readfds,fd_set* writefds,fd_set*exceptfds,const struct timeval*timeout)
这里主要是maxfd,文件描述符的范围,比待检测的更大文件描述符大1
处理过程:也是先将监控的文件添加到文件描述符中,调用select监控,判断文件是否发生变化,但是在底层调用的确是poll方法;首先使用poll_wait将等待队列添加到poll_table中,返回描述符的掩码
poll原型:unsigned int (*poll)(struct file*filp,poll_wait* wait)
看如下一个简单的处理过程
unsigned int mem_poll(struct file *filp, poll_table *wait)
{
struct mem_dev *dev = filp->private_data;
unsigned int mask = 0;
/*将等待队列添加到poll_table */
poll_wait(filp, &dev->inq, wait);
if (have_data)
mask |= POLLIN | POLLRDNORM; /* readable */
return mask;
}
在这里只是添加队列,返回可读可写的掩码,真正阻塞的不是这里,是在do_select(…)函数中,在linux内核fs/select.c里面
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)这个函数
if (file) {
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
if (f_op && f_op->poll) {
wait_key_set(wait, in, out, bit);
mask = (*f_op->poll)(file, wait);
}
这里面是先判断文件存在,然后读取你自己定义的操作设备I/O的f_op函数斗盯,这里有一个默认的mask,接着才判断然后返回描述符mask = (*f_op->poll)(file, wait);用于区分当前哪个被触发了;接着判断f_op&f_op->poll在这里我们默认定义了poll函数,所以这里会进入此判断语句,mask = (*f_op->poll)(file, wait);这个就是调用默认的poll函数进行处理,关键的是如何区分不同的读、写、异常过程?
(mask & POLLIN_SET) && (in & bit),这里面就是对当前的可读、写、异常的&&过程,就是为了判断和区分当前的套接字只是某一个具体的fd_set下;当然某一个套接字也可能同时在可读可写里面,这时候两个会进行判断。
if (retval || timed_out || signal_pending(current))
break;
上面的retval如果为0,且其他也不满足就会导致空循环状态,就处于阻塞状态了
关于Linux下的select/epoll
select这个系统调用的原型如下
之一个参数nfds用来告诉内核
要扫描的socket fd的数量+1
,select系统调用更大接收的数量是1024,但是如果每次都去扫描1024,实际上的数量并不多,则效率太低,这里可以指定需要扫描的数量。
更大数量为1024,如果需要修改这个数量,则需要重新编译Linux内核源码。
第2、3、4个参数分别是readfds、writefds、exceptfds,传递的埋明参数应该是fd_set 类型的引用,内核会检测每个socket的fd,
如果没有读事件,就将对应的fd从第二个参数传入的fd_set中移除,如果没有写事件,就将对应的fd从第二个参数的fd_set中移除,如果没有异常事件,就将对应的fd从第三个参数的fd_set中移除
。这里我们应该
要将实际的readfds、writefds、exceptfds拷贝一份副本传进去,而不是传入原引用,因为如果传递的是原引用,某些socket可能就已经丢失
。
最后一个参数是等待时间,
传入0表示非阻塞,传入>0表示等待一定时间,传入NULL表示阻塞,直到等到某个socket就绪
。
FD_ZERO()这个函数将fd_set中的所有bit清0,一般用来进行初始化等。
FD_CLR()这个函数用来将bitmap(fd_set )中的某个bit清0,在客户端异常退出时就会用到这个函数,将fd从fd_set中删除。
FD_ISSET()用来判断某个bit是否被置1了,也就是判断某个fd是否在fd_set中。
FD_SET()这个函数用来将某个fd加入fd_set中,当客户端新加入连接时就会使用到这个函数。
epoll_create系统调用用来创建epfd,会在开辟一块内存空间(epoll的结构空间)。size为epoll上能关注的更大描述符数,不够会进行扩展,size只要>0就行,早期的设计size是固定大小,但是现在size参数没什么用,会自动扩展。
返回值是epfd,如果为-1则说明创建epoll对象失败
。
之一个参数epfd传入的就是epoll_create返回的epfd。
第二个参数传入对应操作的宏,包括
增删改(EPOLL_CTL_ADD、EPOLL_CTL_DEL、EPOLL_CTL_MOD)
。
第三个参数传入的是
需要增删改的socket的fd
。
第四个参数传入的是
需要操作的fd的哪些事件
,具体的事件可以看后续。
返回值是一个int类型,如果为-1则说明操作失败
。
之一个参数是epfd,也就是epoll_create的返回值。
第二个参数是一个epoll_event类型的指针,也就是传入的是一个数组指针。
内核会将就绪的socket的事件拷贝到这个数组中,用户可以根据这个数组简销拿到事件和消息等
。
第三个参数是maxevents,传入的是
第二个参数的数组的容量
。
第四个参数是timeout,
如果设为-1一直阻塞直到有就绪数据为止,如果设为0立即返回,如果>0那么阻塞一段时间
。
返回值是一个int类型,也就是就绪的socket的事件的数量(内核拷贝给用户的events的元素的数量),通过弯咐告这个数量可以进行遍历处理每个事件
。
一般需要传入 ev.data.fd 和 ev.events ,也就是fd和需要监控的fd的事件。事件如果需要传入多个,可以通过按位与来连接,比如需要监控读写事件,只需要像如下这样操作即可: ev.events=EPOLLIN | EPOLLOUT 。
LT(水平触发),
默认
的工作模式,
事件就绪后用户可以选择处理和不处理,如果用户不处理,内核会对这部分数据进行维护,那么下次调用epoll_wait()时仍旧会打包出来
。
ET(边缘触发),事件就绪之后,
用户必须进行处理
,因为内核把事件打包出来之后就把对应的就绪事件给清掉了,
如果不处理那么就绪事件就没了
。ET可以减少epoll事件被重复触发的次数,效率比LT高。
如果需要设置为边缘触发只需要设置事件为类似 ev.events=EPOLLIN | EPOLLET 即可
。
select/poll/epoll是nio多路复用技术,
传统的bio无法实现C10K/C100K
,也就是无法满足1w/10w的并发量,在这么高的并发量下,在进行上下文切换就很容易将服务器的负载拉飞。
1.将fd_set从用户态拷贝到内核态
2.根据fd_set扫描内存中的socket的fd的状态,时间复杂度为O(n)
3.检查fd_set,如果有已经就绪的socket,就给对应的socket的fd打标记,那么就return 就绪socket的数量并唤醒当前线程,如果没有就绪的socket就继续阻塞当前线程直到有socket就绪才将当前线程唤醒。
4.如果想要获取当前已经就绪的socket列表,则还需要进行一次系统调用,使用O(n)的时间去扫描socket的fd列表,将已经打上标记的socket的fd返回。
CPU在同一个时刻只能执行一个程序,通过RR时间片轮转去切换执行各个程序。没有被挂起的进程(线程)则在工作队列中排队等待CPU的执行,将进程(线程)从工作队列中移除就是挂起,反映到Java层面的就是线程的阻塞。
什么是中断?当我们使用键盘、鼠标等IO设备的时候,会给主板一个电流信号,这个电流信号就给CPU一个中断信号,CPU执行完当前的指令便会保存现场,然后执行键盘/鼠标等设备的中断程序,让中断程序获取CPU的使用权,在中断程序后又将现场恢复,继续执行之前的进程。
如果之一次没检测到就绪的socket,就要将其进程(线程)从工作队列中移除,并加入到socket的等待队列中。
socket包含读缓冲区+写缓冲区+等待队列(放线程或eventpoll对象)
当从客户端往服务器端发送数据时,使用TCP/IP协议将通过物理链路、网线发给服务器的网卡设备,网卡的DMA设备将接收到的的数据写入到内存中的一块区域(网卡缓冲区),然后会给CPU发出一个中断信号,CPU执行完当前指令则会保存现场,然后网卡的中断程序就获得了CPU的使用权,然后CPU便开始执行网卡的中断程序,将内存中的缓存区中的数据包拿出,判断端口号便可以判断它是哪个socket的数据,将数据包写入对应的socket的读(输入)缓冲区,去检查对应的socket的等待队列有没有等待着的进程(线程),如果有就将该线程(进程)从socket的等待队列中移除,将其加入工作队列,这时候该进程(线程)就再次拥有了CPU的使用权限,到这里中断程序就结束了。
之后这个进程(线程)就执行select函数再次去检查fd_set就能发现有socket缓冲区中有数据了,就将该socket的fd打标记,这个时候select函数就执行完了,这时候就会给上层返回一个int类型的数值,表示已经就绪的socket的数量或者是发生了错误。这个时候就再进行内核态到用户态的切换,对已经打标记的socket的fd进行处理。
将原本1024bit长度的bitmap(fd_set)换成了数组的方式传入
,可以
解决原本1024个不够用的情况
,因为传入的是数组,长度可以不止是1024了,因此socket数量可以更多,在Kernel底层会将数组转换成链表。
在十多年前,linux2.6之前,不支持epoll,当时可能会选择用Windows/Unix用作服务器,而不会去选择Linux,因为select/poll会随着并发量的上升,性能变得越来越低,每次都得检查所有的Socket列表。
1.select/poll每次调用都必须根据提供所有的socket,然后就
会涉及到将这个从用户空间拷贝到内核空间,在这个过程中很耗费性能
。但是
其实每次的socket的变化也许并不大,也许就1-2个socket
,但是它会全部进行拷贝,全部进行遍历一一判断是否就绪。
2.select/poll的返回类型是int,只能代表当前的就绪的socket的数量/发生了错误,
如果还需要知道是哪些socket就绪了,则还需要再次使用系统调用去检查哪些socket是就绪的,又是一次O(n)的操作,很耗费性能
。
1.epoll在Kernel内核中存储了对应的数据结构(eventpoll)。我们可以
使用epoll_create()这个系统调用去创建一个eventpoll对象
,并返回eventpoll的对象id(epfd),eventpoll对象主要包括三个部分:需要处理的正在监听的socket_fd列表(红黑树结构)、socket就绪列表以及等待队列(线程)。
2.我们可以使用epoll_ctl()这个系统调用对socket_fd列表进行CRUD操作,因为可能频繁地进行CRUD,因此
socket_fd使用的是红黑树的结构
,让其效率能更高。epoll_ctl()传递的参数主要是epfd(eventpoll对象id)。
3.epoll_wait()这个系统调用默认会
将当前进程(线程)阻塞,加入到eventpoll对象的等待队列中,直到socket就绪列表中有socket,才会将该进程(线程)重新加入工作队列
,并返回就绪队列中的socket的数量。
socket包含读缓冲区、写缓冲区和等待队列。当使用epoll_ctl()系统调用将socket新加入socket_fd列表时,就会将eventpoll对象引用加到socket的等待队列中,
当网卡的中断程序发现socket的等待队列中不是一个进程(线程),而是一个eventpoll对象的引用,就将socket引用追加到eventpoll对象的就绪列表的尾部
。而eventpoll对象中的等待队列存放的就是调用了epoll_wait()的进程(线程),网卡的中断程序执行会将等待队列中的进程(线程)重新加入工作队列,让其拥有占用CPU执行的资格。epoll_wait()的返回值是int类型,返回的是就绪的socket的数量/发生错误,-1表示发生错误。
epoll的参数有传入一个epoll_event的数组指针(作为输出参数),在调用epoll_wait()返回的同时,Kernel内核还会将就绪的socket列表添加到epoll_event类型的数组当中。
关于fd_set结构体linux的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。
香港服务器首选树叶云,2H2G首月10元开通。
树叶云(www.IDC.Net)提供简单好用,价格厚道的香港/美国云服务器和独立服务器。IDC+ISP+ICP资质。ARIN和APNIC会员。成熟技术团队15年行业经验。
文章来源网络,作者:运维,如若转载,请注明出处:https://shuyeidc.com/wp/208941.html<