网络从网卡到线程过程
1.一个以太网接口接收发送到它的单播地址和以太网广播地址的帧。当一个完整的帧可用时,接口就产生一个硬中断,并且内核调用接口层函数leintr
2.leintr检测硬件,并且如果有一个帧到达,就调用leread把这个帧从接口转移到一个mbuf(各层之间传输数据都用这个)链中,构造单独的地址信息etherheaher。
etherinput检查结构etherheaher来判断接收到的数据的类型,根据以太网类型字段来跳转。对于一个IP分组,schednetisr调度一个IP软件中断,并选择IP输入队列,ipintrq。对于一个ARP分组,调度ARP软件中断,并选择arpintrq。并将接收到的分组加入到队列中等待处理。
3.当收到的数据报的协议字段指明这是一个TCP报文段时,ipintrq(通过协议转换表中的prinput函数)会调用tcpinp t进行处理.从mbuf中取ip,tcp首部,寻找pcb,发送给插口层
3.1个关于pcb
每个线程的task_struct都有打开文件描述符,如果是sock类型还会关联到全局inpcb中.
listen某个fd时(new socket()=>bind(listen的端口)=>lisnten())在pcb中该fd是listen状态
3.2in_pcblookup
搜索它的整个Internet PCB表,找到一个匹配。完全匹配获得最高优先级,包含通配的最小通配数量的优先级高。所以当同一个端口已经建立连接后有了外部地址和端口,再有数据会选择该插口。比如当140.252.1.11:1500来的数据直接就匹配到第三个的插口,其他地址的发送到第一个插口。
4.插口层
4.1 listen 如果监听插口收到了报文段
listen状态下只接收SYN,即使携带了数据也等建立连接后才发送,创建新的so,在收到三次握手的最后一个报文段后,调用soisconnected唤醒插口层accept
4.2 插口层accept
while (so->so_qlen == 0 && so->so_error == 0) {tsleep((caddr_t)&so->so_timeo, PSOCK | PCATCH,netcon, 0))}
当so_qlen不是0,调用falloc(p, &fp, &tmpfd)创建新的插口fd,从插口队列中复制,返回,此时该fd也在线程的打开文件中了。调用soqremque将插口从接收队列中删除。
4.3 插口层read,send 从缓冲区复制mbuf。略
5.线程调用内核的accept,从调用开始会sleep直到此时可以返回fd。
【后文若用了epoll在lsfd有事件时通知线程再调用accept会节省调用到sleep时间。】
io传输
DMA
DMA:硬件到内存数据拷贝脱离cpu
CPU与外设之间的数据传送方式有程序传送方式(cpu轮询检查,执行输入指令(IN)或输出指令(OUT))、中断传送方式(外设主动,请求前外设与CPU可以并行工作,需要进行断点和现场的保护和恢复,浪费了很多CPU的时间,适合少量数据的传送)。CPU是通过系统总线与其他部件连接并进行数据传输。
通常系统总线是由CPU管理的,在DMA方式时,由DMA控制器发一个信号给CPU。DMA控制器获得总线控制权,控制传送的字节数,判断DMA是否结束,以及发出DMA结束信号
socket四次数据拷贝
需要复制硬件数据到socket发送时,原方案4次用户空间与内核空间的上下文切换,以及4次数据拷贝(涉及到TCP时,tcp/ip维护send buffer和recv buffer缓冲区——内核空间,需要cpu从用户态复制到内核态,然后DMA通过网卡发送。)
以下叫领拷贝(不拷贝到用户态)
sendfile
2次用户空间与内核空间的上下文切换,以及3次数据的拷贝,但用户不能直接操作写
mmap
user space不拷贝共享kernel space数据:4次用户空间与内核空间的上下文切换,以及3次数据拷贝
mmap是os到用户内存无拷贝,用指针。首先,应用程序调用mmap(图中1),陷入到内核中后调用do_mmap_pgoff(图中2)。该函数从应用程序的地址空间中分配一段区域作为映射的内存地址,并使用一个VMA(vm_area_struct)结构代表该区域,之后就返回到应用程序(图中3)。当应用程序访问mmap所返回的地址指针时(图中4),由于虚实映射尚未建立,会触发缺页中断(图中5)。之后系统会调用缺页中断处理函数(图中6),在缺页中断处理函数中,内核通过相应区域的VMA结构判断出该区域属于文件映射,于是调用具体文件系统的接口读入相应的Page Cache项(图中7、8、9),并填写相应的虚实映射表。
epoll
- 原理
poll/select/epoll的实现都是基于文件提供的poll方法(f_op->poll),该方法利用poll_table提供的_qproc方法向文件内部事件掩码_key对应的的一个或多个等待队列(wait_queue_head_t)上添加包含唤醒函数(wait_queue_t.func)的节点(wait_queue_t),并检查文件当前就绪的状态返回给poll的调用者(依赖于文件的实现)。当文件的状态发生改变时(例如网络数据包到达),文件就会遍历事件对应的等待队列并调用回调函数(wait_queue_t.func)唤醒等待线程。 - 数据结构:
- epoll_crete 创建event_poll,实际上创建了一个socketfd,称epfd。
- epoll_ctl 将回调为ep_poll_callback的节点 加入到epitem对应fd的等待队列中(即sk_sleep的wait_queue_head_t),关联到event_poll的红黑树等结构体中
- epoll_wait 将回调为try_to_wake_up的节点 加入到epfd的等待队列中你。
当发生事件,socket调用ep_poll_callback 会调用try_to_wake_up 进而唤醒wait的线程,向用户赋值rdlist数据,用户线程继续执行
(以scoket为例,当socket数据ready,终端会调用相应的接口函数比如rawv6_rcv_skb,此函数会调用sock_def_readable然后,通过sk_has_sleeper判断sk_sleep上是否有等待的进程,如果有那么通过wake_up_interruptible_sync_poll函数调用ep_poll_callback。从wait队列中调出epitem,检查状态epitem的event.events,若是感兴趣的事情发生,加入到rdllist或者ovflist中,调用try_to_wake_up。) - 数据拷贝:
1.拷贝新添加的events
YSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,struct epoll_event __user *, event)
copy_from_user(&epds, event, sizeof(struct epoll_event)))
2.传给用户的就绪的event
在ep_send_events_proc时
__put_user(revents, &uevent->events。__put_user(epi->event.data, &uevent->data) - 与select区别
重复读入参数,全量的扫描文件描述符;调用开始,将进程加入到每个文件描述符的等待队列,在调用结束后又把进程从等待队列中删除;(每次发生事件fd位图结构改变,重新清除再select)select最多支持1024个文件描述符。从内核实现上,循环所有fd,调用poll->poll_wait加入到fd等待队列。有时间设置mask掩码,循环fd检测有mask返回。(nfds,readxx,writexx)
poll结构上的差异,(fds里直接有事件),不需要遍历所有nfds。无1024限制
epoll 注册事件只需要一次拷贝(增量拷贝,依靠回调),另外返回就绪fd,不需要遍历所有的。把过程拆除带ctl加入poll_wait有事件直接通知,加入rdllist返回 - LT/ET
只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知,LT模式支持阻塞和非阻塞两种方式。epoll默认的模式是LT.ET是当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知,当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知,fd需要非阻塞,要while一直读直到读到EGAIN为止
运行模型
是否立即返回。
阻塞:空cpu,IO阻塞线程
非阻塞
是否由本线程执行
同步IO
异步
1.所有请求单进程/线程
2.一个请求一个线程/进程
accept后一个连接全部过程在一个进程/线程 ,结束后结束线程/进程,每次有新连接创建一个新的进程/线程去处理请求
- 一个连接一个进程:父进程fork子进程 =》fork代价大 百级别
- prefork 多个进程会accept同一个lsfd,linux2.6已经支持就觉多进程同时accept一个时的惊群现象。
- 一个连接一个线程 万级别限制,线程相互影响不稳定
- prethread 多线程共享数据,可以直接accept后分配给线程,也可以多个线程共同accept(accept实现了线程安全?).
3.一个进程/线程处理多个请求
线程池/进程池+非阻塞+IO多路复用 (非阻塞+IO多路复用 少了哪个这种模型都没有意义)
reactor 监听所有类型事件,区分accept和业务处理
- 单reactor单线程。reactor是负责IO事件监听
- 单reactor多线程 接收后read后给子线程处理,处理后返回发送;主线程负责所有IO事件处理
- 多reactor多进程 nginx没有accept后分配,而是子进程自己listen,accept。
- 多reactor多线程 主accept接收后把fd给子线程,子线程读-处理-写(更简单,无需传递读写数据)memchache、netty
这个网上的图是错的。 accept后所有的读写处理都在一个线程中,无共享数据需要传递
总结下:
基本上是accept肯定要在一个线程中,因为只有一个fd。
1)单reactor单线程 accept+read/process/send
2)单reactor多线程 accept+read/send =》多process
3)多reactor多线程 accepct=>多read/process/send
4)另一种 accepct[0号]=>子多read/send =》多process 当只有一个时退化为单reactor多线程。线上就这个。
适用范围:
假设4个请求并发连接,2个线程
若p是瓶颈,比如p1占用4个格子
1)单reactor多线程r1->r2->r3->r4->s1->s2->s3->s4 p1w|w|w|w|->p3w|w|w|w| p2w|w|w|w|->p4w|w|w|w|
2)多reactor多线程
r1->p1w|w|w|w|->r3->p3w|w|w|w|->s1->s3 r2->p2w|w|w|w|->r4->p4w|w|w|w|->s2->s4
此时单reactor多线程更快。多reactor多线程编写简单
若r是瓶颈(比如占3个,4个换行了=。=)
1)单reactor多线程r1w|w|w|->r2w|w|w|->r3w|w|w|->r4w|w|w|->s1w|w|w|->s2w|w|w|->s3w|w|w|->s4w|w|w| p1-> p3 p2-> p4
2)多reactor多线程
r1w|w|w|->p1->r3w|w|w|->p3->s1w|w|w|->s3w|w|w| r2w|w|w|->p2->r4w|w|w|->p4->s2w|w|w|->s4w|w|w|
此时多reactor多线程快
最后一种模式分别
r1->r3 ->s1 ->s3 p1w|w|w|w|->p3w|w|w|w| r2->r4 ->s2 ->s4 p2w|w|w|w|->p4w|w|w|w| r1w|w|w|->r3w|w|w|->s1w|w|w|->s3w|w|w| p1-> p3 r2w|w|w|->r4w|w|w|->s2w|w|w|->s4w|w|w| p2-> p4
- 结论:
若读写为瓶颈建议多reactor多线程。
若处理为瓶颈建议单reactor多线程,
用最后一种混合需要评估下,如果处理为瓶颈还可以考虑下,io为瓶颈就不用了,因为并没有快多少,反而编程麻烦。
4.proactor
与reactor区别是reactor是同步读写,preactor是异步读写
- 在Reactor中实现读
注册读就绪事件和相应的事件处理器。
事件分发器等待事件。
事件到来,激活分发器,分发器调用事件对应的处理器。
事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。 - 在Proactor中实现读:
处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。
事件分发器等待操作完成事件。
在分发器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分发器读操作完成。
事件分发器呼唤处理器。
事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分发器。
关于数据共享
早在1973年,actor模式被提出(跟图灵机一个级别的模式,只是个模式),主要是对立于数据共享加锁的。一个是分布式计算中没办法很好的共享数据,另一个是共享数据加锁会阻塞
线程(单机的话)/请求超时(分布式),还有个不重要的有时候锁不住(比如cpu的多级cache,没个线程里的cache不同步),因此采用一种非阻塞和通信的方式,数据发送到actor排队
后即返回不加锁,通过通信传递改变,actor是整个数据+行为(对象)的组合
,不仅仅负责数据的锁,actor之间要做到无共享,发给actor的自己复制一份新的,actor可以继续调actor,所以要都无共享
。(这么说非阻塞IO,select等都是借鉴的这个。)
actor行为
1.Actor将消息加入到消息队列的尾部。
2.假如一个Actor并未被调度执行,则将其标记为可执行。
3.一个(对外部不可见)调度器对Actor的执行进行调度。
4.Actor从消息队列头部选择一个消息进行处理。
5.Actor在处理过程中修改自身的状态,并发送消息给其他的Actor。
为了实现这些行为,Actor必须有以下特性:
● 邮箱(作为一个消息队列)
● 行为(作为Actor的内部状态,处理消息逻辑)
● 消息(请求Actor的数据,可看成方法调用时的参数数据)
● 执行环境(比如线程池,调度器,消息分发机制等)
● 位置信息(用于后续可能会发生的行为)
另外一种:CSP,不要通过共享内存来通信,而应该通过通信来共享内存的思想
,Actor 和 CSP 就是两种基于这种思想的并发编程模型.Actor 模型的重点在于参与交流的实体,而 CSP 模型的重点在于用于交流的通道.channel。channel共享带锁,不再扩展了。
actor的设计也要抽象好,比如
比如左右两个叉子,如果加锁。锁(左右)/ 为了并发,单独左右。actor要左右组合在一起,每次判断左叉子和右叉子都有返回成功。
zmq 每个线程绑定一个cpu,线程之间不会共享session,不需要加锁。每个连接的操作都在一个worker中.通信传递数据。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。