LINUX 精通 2
day14 20240513
day15 20240514
算法刷题:2维前缀和,一二维差分 耗时 135min
习题课 4h
课程补20240425 耗时:4h
课程链接地址
回顾
- 怎么学0voice课
- 网络io——一请求一线程,一个client一个连接再accpet分配io fd文件描述符
注意:rm -rf
是一个非常强大和危险的命令,它会递归地删除目录及其内容,而不进行任何确认。请谨慎使用此命令,以免意外删除重要文件或系统关键组件
每次上课前统一一下代码
-
gitlab.0voice.com
找到代码,然后在Linux终端里
git clone
可以挂梯子, 用户名是邮箱在本地下连ftp shell好像不太行
-
gcc -o networkio networkio.c -lpthread
问题
-
client断开后问题
运行以后./networkic 连三个 发三个,然后断开一个,会一直send recv 一瞬间cpu占用100%, 因为没有处理断开的
我没碰到,因为老师改过了void *client_thread加了count==0处理的
if (count == 0) { // disconnectprintf("client disconnect: %d\n", clientfd);close(clientfd);break;}
-
fd就是网络io 是int型
开了sockfd
fd(不论是sockfd 还是clientfd) 从3开始,0 1 2系统默认stdin stdout stderror;往上增加
ls /dev/fd
看ulimit -a文件描述符fd数量max (open files)
为什么能一直增加:
linux下操作,一切皆是文件FD(file descriptor)
可以隔段时间再send
-
client断开后,隔段时间再连接,fd变了吗
变了
disconnect以后就
close(clientfd)
了!!!!4没了,被回收了,变成了7
等一段时间,又变成4了
io回收时间 系统默认60s,set可以设置time_wait
2.1.2 事件驱动reactor的原理与实现
还是用我自己的版本
一请求一线程
优点:代码逻辑简单
缺点:不利于并发 1k,通过创建线程实现并发
所以用多路复用io
调试技巧
在命令行打man select函数名
就能看解释
select poll epoll
fdset
-
到底是什么东西?
-
答案:它是比特位集合
把fd放一起的set集合
干嘛的:比如你时间管理大师,处了好多个fd 对象
-
为什么要设置一个集合fdset, 然后传进select,传来传去
传入3456,系统返回34可读
所有通讯底层的server io多路复用都是这么写的
云里雾里的头痛,乱七八糟的,send可以,但是收不到!!!!我 爆炸了裂开
现在是main里一个线程,多路io fd,fd间不影响!!
可读返回,不可读阻塞在select,对着标准答案改了终于行了
-
fd_set结构体
select头文件里就1个struct, 4个宏定义,1个函数不难的
宏定义FD_ZERO, FD_SET, FD_CLR,
select()函数的参数:可读 可写,错误,
timeout=null 默认一直阻塞, 如果阻塞超过市场就往下走,可以做一个定时器,就是为了切换线程,等待就绪再次被执行
-
-
大小
fd_setsize大小可以改
在posix_types.h里看到
fd_setsize = 1024
8因为一个字节byte = 8bit
sizeof(long)因为前面是unsigned long 所以要除它, 假设long是4 byte
所以一个fd大小是1024/(8*4) = 32 byte
select
特点/运行机制
-
每次调用select需要把fd_set集合,从用户空间copy到kernel内核空间
-
maxfd, 为了遍历fd是否set置一了,设置的最大的fd,需要人为设置
for( int i = 0; i< maxfd; i ++)
ps:rfds, rset区别
fd_set rfds, rset;
//rfds返回数据dataset, 是应用层的,用户设置的
// rset是复制rfds的, 用于被复制到内核空间,用于判断的
优点:实现只用一个thread进程就能多路io复用
缺点: 参数太多,麻烦
#elsefd_set rfds, rset; //rfds返回数据dataset, 是应用层的,用户设置的// rset是复制rfds的, 用于被复制到内核空间,用于判断的FD_ZERO(&rfds); //先清空FD_SET(sockfd, &rfds); // 再把sockfd 设置在可读rfds里,置1int maxfd = sockfd; //用来方便遍历set用到的最大值while(1){rset = rfds; //关联,// maxfd +1因为下标从0开始,数量比最后多1// int select(int nfds, fd_set *readfds, fd_set *writefds,// fd_set *exceptfds, struct timeval *timeout);int nready = select(maxfd + 1, &rset, NULL, NULL, NULL); //返回就绪的fd数量,就绪的bit位是1if(FD_ISSET(sockfd, &rset)){//sockfd位是否置1// accept 如果监听的sockfd置1了,就开始accept连接int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("accept finished: %d\n", clientfd);FD_SET(clientfd, &rfds); //有一个fd,就set一下,maxfd也变了if(maxfd < clientfd) maxfd = clientfd; //clientfd当client断连了,会被回收,所以要判断一下}// recvint i = 0;for(i = sockfd +1; i<= maxfd; i++){ //i就是fdif (FD_ISSET(i, &rset)){//判i可读rset吗char buffer[1024] = {0};int count = recv(i, buffer, 1024, 0); //Read N bytes into BUF from socket FD.if (count == 0) { // disconnectprintf("client disconnect: %d\n", i);close(i);FD_CLR(i, &rfds); //FD_CLR是一个宏,用于从fd_set数据结构中清除指定的文件描述符continue;}printf("RECV: %s\n", buffer);count = send(i, buffer, count, 0);printf("SEND: %d\n", count);}}}#endif
poll
-
struct pollfd里
比如上面传入3456 只返回34可读
fd 是哪一个fd
events 传入的事件
revents 返回的事件
基本代码和select差不多,就是4个宏还有select函数要改成poll才有的 写法
别进错文件夹编译
gcc -o networkio networkio.c ./networkio
开三个网络助手——connect——send——recv吗
成功啦啦啦啦啦啦啦
-
总结poll有什么呢
-
pollfd是个结构体:fd 是哪一个fd;events 传入的事件;revents 返回的事件
-
宏定义:pollin可读,pollout等等
-
poll函数是系统调用 每次把fds copy到内核kernel里
系统用for遍历maxfd个数量,判断这个io fd是否就绪
-
-
poll有什么独特使用场景
-
底层逻辑类似select参数更少
-
问: 假设5个fd一起来,阻塞,假设都不能可读一直等,直到1可读立即返回?
答:不对,因为内核做不了微秒级,第一次与第二次进while的select往下几乎没有处理上的差别
没懂???
-
#else// 进pollfd 看参数struct pollfd fds[1024]= {0}; //fdset就是fds[sockfd].fd = sockfd;fds[sockfd].events= POLLIN; //pollin就是可读,设置为POLLIN表示对该文件描述符上是否有可读数据感兴趣int maxfd = sockfd; //来遍历用的,检查哪个fd set了 while(1){int nread = poll(fds, maxfd + 1, -1);//set, set大小, timeout =-1一直阻塞等待if (fds[sockfd].revents & POLLIN){// pollin是x十六进制0x0001,变成8位2进制00000001// 如果有可读的,用accept处理分配io,复制上面int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("accept finished: %d\n", clientfd);// FD_SET(clientfd, &rfds); //select的这句改成poll的下面两行fds[clientfd].fd= clientfd;fds[clientfd].events=POLLIN;if(maxfd < clientfd) maxfd = clientfd; //clientfd当client断连了,会被回收,所以要判断一下}// 抄上面recvint i = 0;for(i = sockfd +1; i<= maxfd; i++){ //i就是fd// if (FD_ISSET(i, &rset)){//判i可读rset吗 select有,poll没有if(fds[i].revents & POLLIN){ //判i可读吗,和Pollin位与char buffer[1024] = {0};int count = recv(i, buffer, 1024, 0); //Read N bytes into BUF from socket FD.if (count == 0) { // disconnectprintf("client disconnect: %d\n", i);close(i);// FD_CLR(i, &rfds); //FD_CLR是fdset里的一个宏,select有;poll没有fds[i].fd= -1; //因为从0开始,置-1fds[i].events= 0;continue;}printf("RECV: %s\n", buffer);count = send(i, buffer, count, 0);printf("SEND: %d\n", count);}}}
epoll
linux 2.4以前,没有听过linux做server的,也没有云主机。当时server都是Windows,unix,十几年后现在云主机很多系统都是Linux,因为linux2.6以后引入epoll,server对io的数量更多
为什么?select与poll底层都需要进while的select/poll阻塞检查,再for判断 accept recv ,epoll不用
#elseint epfd = epoll_create(1);struct epoll_event ev; //构建事件,只用来add和delete,control里没用ev.events = EPOLLIN;ev.data.fd = sockfd;epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); //controlwhile(1){struct epoll_event events[1024] = {0};int nready = epoll_wait(epfd, events, 1024, -1);int i = 0;for (i = 0;i < nready;i ++) {int connfd = events[i].data.fd;if (connfd == sockfd) {// accept int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);printf("accept finshed: %d\n", clientfd);// 创建events, 添到ctl里ev.events = EPOLLIN;ev.data.fd = clientfd;// 这里ev不写也可以epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);} else if (events[i].events & EPOLLIN) {char buffer[1024] = {0};int count = recv(connfd, buffer, 1024, 0);if (count == 0) { // disconnectprintf("client disconnect: %d\n", connfd);close(connfd);// 改了这里epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);// epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, &ev); 也可以continue;}printf("RECV: %s\n", buffer);count = send(connfd, buffer, count, 0);printf("SEND: %d\n", count);}}}
总结
-
就一个struct结构体+三个函数,给应用层提供的接口
-
int epoll_create(int size);
知识点1:epoll_create返回传入都是int。epoll一开始size代表一次性就绪的io数量,后来就绪从数组改成链表,此时size没作用,只要size不为0,效果都一样。 size为了兼容老版本留下来了
-
为什么epoll不用遍历?
住户是IO或者fd,epoll类似快递员,但是去丰巢,由io自己去丰巢,事件events 有两个= 收+寄;poll、select是每家敲门,没法时时敲门还
- epoll_create:用struct组织200总集200个住户IO;用struct组织快递柜丰巢——就绪,并聘请快递员
- epoll_ctl: 住户搬走EPOLL_CTL_DELET,搬进ADD,换楼层位置MOD
- epoll_wait: 快递员多久去一次丰巢,timeout市场;events是小车取出就绪内容,maxevent是小车多大
-
为什么能大并发?
-
select(maxfd, rfds, wfds, efds, err);五个参数
if 100万 io,需要把100万全部copy到对应fds里判断可读、写…
-
但是epoll不用每次从用户应用层copy到内核里,epoll create是一个个添加到内核里积累起来,有读写事件来了,wait就从就绪里操作
-
就绪里是真正处理的事件:微信号称3亿用户同时在线,就是IO整集大小,但是不代表server处理同时发消息
每个client对应io,可能就绪队列在发消息的才一百万不到
-
-
思考题:
- 整集用什么数据结构存
- 数组:将整数按顺序存储在数组中,并通过索引访问。这种方式简单直接,但插入和删除操作的时间复杂度较高。
- 链表:将每个整数存储在链表节点中,并使用指针连接节点。这种方式对于频繁的插入和删除操作更为高效,但随机访问的性能较差。
- 哈希集合:利用哈希函数将整数映射到不同的桶中,在每个桶内使用链表或红黑树来处理冲突。这种方式可以实现快速查找、插入和删除操作。
- 就绪用什么数据结构存
- 位图(Bitmask):使用一个二进制位代表一个文件描述符,当某个文件描述符就绪时,相应位设置为1。该方法适用于文件描述符数量较少且连续排列的情况。
- 数组:将就绪的文件描述符存储在数组中。该方法适用于文件描述符数量不多且无需频繁变动的情况。
- 数据结构依赖具体的多路复用机制:例如,epoll使用红黑树来管理就绪事件,将文件描述符作为节点进行存储;而select和poll则使用fd_set结构体数组来存储就绪文件描述符。
- 整集用什么数据结构存
引出reactor反应堆
-
epoll:io数量很多;
poll:io少, <10
select: 无poll epoll 比如Linux2.4前
-
都是对网络应用层的IO事件处理4种,根本没有对用户层的业务service处理还,只处理了IO里的事件,告诉你吃饭events事件了,没告诉你什么饭怎么吃具体的service
-
过程:server listen——client connect——server accept——client send ——server receive同时回传
-
事件:
所以是IO事件触发——水平触发,边沿触发
核心是events事件,一个IO的生命周期=无数多个events
server关心events,不是具体io,对不同events执行不同callback回调函数cb,模块化!!!
-
reactor:核反应堆=不同IO事件处理callback回调函数 cb的集合
封装起来,给以后用户层service用,更符合人的逻辑
**什么是reactor:**注册一个events,当events发生,就从reactor查返回回调函数,做反应(类似留下名片只要有事就call 你)
为什么要用reactor因为可以更好关注事件而不是io,io太多也不是都活着,从io管理➡️ 事件管理
杂
网络IO
- accept——》listenfd/sockfd
- send/recv——》clientfd
网络应用是所有服务的基石
ps:
要用形象的东西,记忆更牢,也不反人性,少掉头发少烧脑
5/15晚上把github同步自己代码搞定!!!