linux文件描述符(file discription)
1、文件描述符是一个简单的整数,用以标明每一个被进程所打开的文件或socket
2、每一个进程都有自己的文件描述符集合。
3、当创建进程时,通常默认会有3个文件描述符(0,1,2),0代表标准输入,1代表标准输出,2代表标准错误,它们统称为标准IO,所以如果进程通过open打开一个文件的时候,文件描述符会从3开始,fd的值其实就是进程中打开文件列表的文件描述符集合的下标索引。
4、由于文件描述符在一个进程中是特有的,因此不能在多个进程中间实现共享,而唯一的例外是在父/子进程之间,当一个进程调用fork时,调用fork时打开的所有文件在子进程和父进程中仍然是打开的,而且子进程写入文件描述符会影响到父进程的同一文件描述符,反之亦然
5、Unix操作系统通常给每个进程能打开的文件数量强加一个限制(1024),ulimit -n查看系统默认的文件描述符
6、基于文件描述符的输入输出函数:
open:打开一个文件,并指定访问该文件的方式,调用成功后返回一个文件描述符。
creat:打开一个文件,如果该文件不存在,则创建它,调用成功后返回一个文件描述符。
close:关闭文件,进程对文件所加的锁全都被释放。
read:从文件描述符对应的文件中读取数据,调用成功后返回读出的字节数。
write:向文件描述符对应的文件中写入数据,调用成功后返回写入的字节数。
ftruncate:把文件描述符对应的文件缩短到指定的长度,调用成功后返回0。
lseek:在文件描述符对应的文件里把文件指针设定到指定的位置,调用成功后返回新指针的位置。
fsync:将所有已写入文件中的数据真正写到磁盘或其他下层设备上,调用成功后返回0。
fstat:返回文件描述符对应的文件的相关信息,把结果保存在struct stat中,调用成功后返回0。
fchown:改变与打开文件相关联的所有者和所有组,调用成功后返回0。
fchmod:把文件描述符对应的文件的权限位改为指定的八进制模式,调用成功后返回0。
flock:用于向文件描述符对应的文件施加建议性锁,调用成功后返回0。
fcntl:既能施加建议性锁也能施加强制性锁,能建立记录锁、读取锁和写入锁,调用成功后返回0。
dup:复制文件描述符,返回没使用的文件描述符中最小的编号。
dup2:由用户指定返回的文件描述符的值,用来重新打开或重定向一个文件描述符。
select:同时从多个文件描述符读取数据或向多个文件描述符写入数据
7、简单归纳:fd只是一个整数,在open时产生。起到一个索引的作用,进程通过PCB中的文件描述符表找到该fd所指向的文件指针filp。
select、poll、epoll之间的区别(搜狗面试) - aspirant - 博客园
select
【硬核教程】IO多路复用底层原理全解,select,poll,epoll,socket,系统中断,进程调度,系统调用_哔哩哔哩_bilibili
- 监听方式:它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
- 文件描述符的限制:select用一个bit序列去记录监听的文件描述符(长度固定为1024的bit序列),监听一个socket,就将序列上某一位置为1,所以select最多可以监听1024个socket连接。
- 事件read:select要让用户来判断哪些socket产生了事件,因此每次都要将binmap复制到用户空间,用户处理完后将binmap归位后又赋值到内核空间,产生了大量的拷贝。
- 参数:下面的select函数是用户自己去调用的。maxfdp1是指定OS将文件描述符都设置到fd_set的哪些范围,这样maxfdp1后面的位就不用检查了;fd_set就是binmap,用户要传3个fd_set,去接收OS监听到的socket事件,readset接收read事件,writeset接收write事件,exceptset接收异常事件。
for (int=0;i<5;i++){
// 伪代码,socket server接收5个client连接.fds里面存的是建立连接的socket文件描述符
fds[i] = accept(serverSocketFd);
// max为最大的文件描述符
int max;
if (fds[i] > max) {
max = fds[i];
}
}
// read binmap
int rset[1024];
while (1){
// 把rset的所有位置0
FD_ZERO(&rset);
// 把socket位置1
for (i=0;i<5;i++){
FD_SET(fds[i], &rset);
}
print("round again");
select(max+1, &rset, NULL, NULL, NULL);
for (i=0;i<5;i++){
// 判断这几个socket的fd有没有被置位,有说明监听到了对应socket的read事件。
if (FD_ISSET(fds[i], &rset)) {
memset(buffer, 0, MAXBUF);
// 从就绪的socket中read数据
read(fds[i], buffer, MAXBUF);
print(buffer);
}
}
}
poll
与select一样,只是没有了文件描述符1024的限制,使用链表来记录文件描述符。
epoll
视频:仅有30%的人了解的Linux网络高并发技术之epoll_哔哩哔哩_bilibili
- 支持的文件描述符上限是整个系统最大可以打开的文件数目。例如,在1GB内存的机器上,这个限制大概为10万左右。
- epoll不是轮询,而是采用通知机制。每个socket都有一个callback函数,只有活跃的socket才会主动去调用callback函数,当有事件(如socket读缓冲区被写入了数据)产生时,会调用回调函数去通知epoll。也就是说,epoll只管你“活跃”的连接,而跟连接总数无关。
- 事件read:epoll来监听socket,当事件发生后将对应的事件信息直接复制到公共内存中,用户直接取就可以,避免了用户来判断和频繁的数据拷贝。通过内核与用户空间共享(mmap())同一块内存来避免拷贝。
mmap(一种内存映射文件的方法)_百度百科 (baidu.com)
mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回地址空间的操作只在内存中有意义,只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小。
当事件触发时,epoll会将数据复制到events(struct epoll_events)中,然后epoll_wait()会返回事件数量,程序直接从event中读取数据即可。
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
每一个epoll对象都有一个独立的eventpoll结构体,epoll中内置了红黑树和双向链表,红黑树用于存放通过epoll_ctl方法向epoll对象中添加进来的事件,双向链表用来存放满足条件的事件。
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
struct epoll_event{
uint32_t events; /* 具体监听的事件如:EPOLLIN*/
epoll_data_t data; /* User data variable,可以存放监听的socket的fd */
}
typedef union epoll_data{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
为什么使用红黑树?
因为epoll_ctl方法可以向epoll中添加、修改、删除事件,在添加的过程中也要判断是否有重复的事件(等于要遍历一遍),因此事件的操作涉及到增删改查,红黑树作为弱平衡的排序二叉树,查询效率为lgn,同时因为只追求若平衡,插入和删除的效率也非常高,平均调整的次数在三次左右。
为什么使用双向链表?
因为双向链表插入和删除非常的方便,当事件发生时将事件信息拷贝到用户态,然后将事件从链表删除;事件发生时又要插入。
epoll用法:
第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。参数size是你在epoll上能够监听的socket的数量。
第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。参数fd表示监听的socket fd,参数event表示监听的socket的什么事件(EPOLLIN,EPOLLOUT等)。
第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。
仔细体会 epoll中的et lt模式 - 克莱尔孙 - 博客园
- 边沿触发(edge triggered),就是在事务的两个状态交替的边沿触发,对socket来讲就是的新的数据来的时候触发,如果是上一次的数据,你没有收完 则不会再次提醒。除非有新的数据到来。
- 电平触发(levle triggered),所谓电平触发只要是事务的某一状态出现就触发,对sockets来讲就是只有内核缓冲区中有数据,就会触发。而不管这数据是你上次没收完,还是新来数据。
// 指定水平触发 38 /* 注册文件描述符到epoll,并设置其事件为EPOLLIN(可读事件) */ 39 void addfd_to_epoll(int epoll_fd, int fd, int epoll_type, int block_type){ 41 struct epoll_event ep_event; 42 ep_event.data.fd = fd; 43 ep_event.events = EPOLLIN; 44 45 /* 如果是ET模式,设置EPOLLET,使用“|”就可以监听多个事件 */ 46 if (epoll_type == EPOLL_ET) 47 ep_event.events |= EPOLLET; 48 49 /* 设置是否阻塞 */ 50 if (block_type == FD_NONBLOCK) 51 set_nonblock(fd); 52 53 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ep_event); }
一:代码场景:
- 1:ET模式,得到可读事件,但是不去read数据,看epoll_wait以后是否有事件提醒。
2:LT模式,得到可读事件,但是不去read数据,看epoll_wait以后是否有事件提醒。
- 1:ET模式,没有及时读取数据,再次有新数据到来,能不能继续读取上次的数据。
验证结果:ET模式中,接收到新数据,给数据提醒,无论读不读数据,读取多少,都只提醒一次。如果没有及时读取,则下次有新数据到来,则读取上次数据。
LT模式中,接收到新数据,给数据提醒,只要不读完数据,一直提醒。
二:如何处理ET模式场景
解决此问题有两种方法:
1.采用非阻塞函数,将数据收完后,再次调用epoll_wait()。
为什么要采用非阻塞的方式读取数据?
- ET模式下每次write或read需要循环write或read直到errno=EAGAIN(再试一次。常见于在非阻塞模式下读取文件,当没有数据可读时,errno=EAGAIN)错误。以读操作为例,这是因为ET模式只在socket描述符状态发生变化时才触发事件,如果不一次把socket内核缓冲区的数据读完,会导致socket内核缓冲区中即使还有一部分数据,该socket的可读事件也不会被触发。
- 根据上面的讨论,若ET模式下使用阻塞IO(最后read不到数据会阻塞而不是返回EAGAIN),则程序一定会阻塞在最后一次write或read操作,因此说ET模式下一定要使用非阻塞IO。
errno:errno 是记录系统的最后一次错误代码。代码是一个int型的值,在errno.h中定义。
2.在接收数据后,不管有没有收完,都调用 epoll_ctl() with EPOLL_CTL_MOD,这样相当于重新设置事件,就可以收到数据。
int epfd = epoll_create(10);
...
struct epoll_event envent[5];
for (int i=0;i<5;i++){
static struct epoll_event ev;
// socket server接收连接(简化了)。封装epoll_event,监听的时间(EPOLLIN),监听的socketfd
en.data.fd = accept(socketServerFd);
ev.event = EPOLLIN;
// 将epoll_event添加到epoll中
epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);
}
while (1){
prints("round again");
// 等待read事件就绪,最大接收5个读就绪socket的数据,非阻塞调用。返回就绪到的socket个数
nfds = epoll_wait(epfd, events, 5, 10000);
for (i=0;i<nfds;i++){
memset(buffer, 0, MAXBUF);
// 从就绪的socket缓冲区中读数据
read(events[i].data.fd, buffer, MAXBUF);
prints(buffer);
}
}
select、epoll的区别
- select、poll能监听的socket进程的个数是单个进程能监听的上限1024(可以修改);poll才采用链表来维护文件描述符列表,监听的数量只跟内存有关,也是系统的上限;epoll能监听的个数是整个系统的上限,1G内存大概是10万个。
- 当IO中断产生时,select、poll在内核中采用主动轮训所有socket的方式来判断哪些socket事件就绪;epoll采用异步回调的方式,就绪socket进程会主动通知epoll,epoll只用关心活跃的socket。
- 当有就绪事件产生时,select、poll是通过内存拷贝的方式将就绪列表从内核空间拷贝到用户空间;epoll是通过mmap开辟一块共享空间(用户程序创建的epoll_event数组),把就绪事件写到共享空间中,用户进程直接读取即可。