其他分享
首页 > 其他分享> > 操作系统——网络系统

操作系统——网络系统

作者:互联网

常见OSI七层:物数网传会表应

四层:应用层(负责向用户提供一组应用程序,如HTTP/DNS/TCP);传输层(负责端到端通信,如TCP/UDP);网络层(负责网络包的封装、分片、路由、转发,比如IP/ICMP);网络接口层(负责网络包在物理网络中的传输,比如网络包的封帧、MAC寻址、差错检测,通过网卡传输网络帧)

Linux网络协议栈:应用层到传输层,加了TCP;传输层到网络层,加了IP头;到网络接口层,加了帧头和帧尾。

有MTU最大传输单元,单次传输的最大IP包的大小。

当网络包超过MTU,就会在网络层分片,MTU越小吞吐能力越差。

应用程序——系统调用——lvs——socket——TCP/UDP/ICMP——IP——ARP——MAC——网卡驱动程序——网卡。

应用程序需要通过系统调用,来跟socket层进行数据交互。

网卡负责接收和发送网络包,接收到的时候,会通过DMA技术,把网络包放到环形缓冲区(Ring buffer)

Linux接收网络包的流程:

以前是网卡发中断,中断太多了影响CPU效率。

所以引入了NAPI机制,混合中断和轮询接收网络包,不采用中断的方式读数据,而是首先采用中断唤醒数据接收的服务程序,然后poll的方法来轮询数据。

有网络包到达,网卡发起硬件中断,执行网卡硬件中断处理函数,中断处理函数处理完需要暂时屏蔽中断,然后唤醒软中断来轮询处理数据,直到没有新数据时才恢复中断,这样一次中断处理多个网络包。

第一步就是从环形缓冲区中拷贝数据到内核缓冲区中,作为一个网络包给网络协议栈进行逐层处理。最后,应用程序调用socket接口,从内核的socket接收缓冲区中读取新到来的数据到应用层。

Linux发送网络包的流程:

首先,应用程序调用socket发送数据包的接口,系统调用使得从用户态进入内核态的socket层,将应用层的数据拷贝到socket的发送缓冲区。

接下来,网络协议栈从socket发送缓冲区取出数据包,从上到下传输。最后,触发软中断告诉网卡驱动程序,放到网卡队列,用物理网卡发出去。

 

硬盘很慢,速度和内存相差十倍以上。优化磁盘速度有很多方案:零拷贝、直接IO、异步IO等,这些优化的目的都是为了提高吞吐量。(操作系统内核中的磁盘高速缓存区可以有效减少磁盘访问次数)

如果没有DMA,磁盘控制器有磁盘控制器缓冲区,CPU有PageCache,read系统调用都要CPU亲自搬送数据。有了DMA,将数据从磁盘控制器缓冲区搬送到系统内核缓冲区的工作就是DMA做,CPU只要调用返回。

传统的文件传输:服务器如果要提供文件传输功能,就要从磁盘上读取,然后通过网络协议传给客户端。

用户态有用户缓冲区,内核态有缓存区和socket缓冲区。然后两边是磁盘文件和网卡。上述就是要从磁盘文件到网卡。

从磁盘文件通过DMA拷贝到内核缓存区;通过CPU拷贝到用户缓冲区;通过CPU拷贝到内核态的socket缓冲区;通过DMA拷贝到网卡。 

 用户进程从磁盘read,write到网卡发送出去,发生了四次用户态与内核态的上下文切换,还发生了四次数据拷贝,两次CPU两次DMA。

要想提高文件传输的性能,就需要减少用户态和内核态的上下文切换和数据拷贝的次数。

(1)减少上下文切换,就是减少系统调用的次数。读取磁盘数据的时候,之所以发生上下文切换,是因为用户空间没有权限操作磁盘或网卡,内核的权限最高,所以要交给内核,就需要系统调用。

(2)减少数据拷贝,从内核到用户,从用户到内核,这两个步骤是没有必要的。因此,用户的缓冲区存在是没有必要的。

零拷贝:mmap+write、sendfile

mmap()系统调用会直接把内核缓冲区里的数据映射到用户空间(应用进程和操作系统内核共享这个缓冲区),这样内核与用户区就少了一次拷贝操作。

通过使用mmap()代替read(),可以减少一次数据拷贝的过程。但这并不是最理想的零拷贝,因为仍然需要通过CPU把内核缓冲区的数据拷贝到socket缓冲区里,而且仍然需要四次上下文切换,因为系统调用还是两次。

 

 sendfile是专门发送文件的系统调用函数,可以直接把内核缓冲区的数据拷贝到socket缓冲区中,不用再拷贝到用户态。

这样只要一次系统调用(从用户到内核、从内核到用户)这样就只有两次上下文切换,三次数据拷贝。

 

 这还不是真正的零拷贝技术,如果网卡支持SG-DMA技术,我们可以进一步减少通过CPU把内核缓冲区中的数据拷贝到socket缓冲区的过程。

两次上下文切换、两次数据拷贝。总体来看,零拷贝技术可以把文件传输的性能提高至少一倍以上。Kalfa就用到了零拷贝。

 

 上面提到了内核态的缓冲区就是磁盘高速缓存PageCache。零拷贝技术采用了PageCache技术。因为读写磁盘很慢,所以应该想办法把读写磁盘换成读写内存,于是通过DMA技术把磁盘数据搬到内存里,就可以用读内存替换读磁盘。但是内存空间远比磁盘小,智能拷贝一小部分数据。根据局部性,刚被访问的数据在短时间内再次访问的概率很高。读磁盘的时候,先在PageCache找,如果存在立即返回;如果没有,从磁盘中读取,然后缓存在PageCache里。

但是传输大文件的时候,PageCache不起作用,白白浪费了DMA拷贝的数据,造成性能降低。大文件放进去就没有小文件的空间了,而且大文件也不是热点访问数据。所以大文件传输不用零拷贝。

同步IO如下图。

异步IO如下图。

 

 异步IO没有用到PageCache。绕过PageCache叫做直接IO。使用PageCache叫做缓存IO。

在高并发场景下,针对大文件传输的方式,应该使用直接IO+异步IO的方式来代替零拷贝技术。

 

IO多路复用技术

socket技术就像在客户端和服务器都开了一个网口,然后用一根网线把两端连接起来。

服务端sbla(socket/bind/listen/accept)

首先调用socket函数,创建指定网络协议、传输协议的socket;接着调用bind函数,给这个socket绑定一个端口和IP地址(当内核收到报文,通过端口号找到应用程序,然后传递数据。一台及其有很多网卡,每个网卡有对应的IP地址,应绑定一个网卡时,内核收到网卡的数据包才会发送给我们);接着调用listen函数进行监听;最后调用accept函数,从内核获得客户端连接,如果没有客户端连接,会阻塞等待客户端到来(所以有多路IO复用技术)

客户端sc(socket/connect)

connect指定服务端的IP和端口号,开始TCP三次握手。

TCP连接的过程中,服务器内核为每一个socket维护了两个队列。一个是还没完全建立连接的队列,叫做TCP半连接队列,服务器处于syn_rcvd。一个是已经建立连接的队列,叫做TCP全连接队列,服务器处于established。

 

Q:你知道服务器单机理论最大能连接多少客户端吗?

TCP连接是由四元组去欸的那个的,本地IP、本地端口、对端IP、对端端口。

服务端的IP和端口是确定的。所以最大TCP连接数=客户端IP数*客户端端口数

对于IPV4,客户端IP数最多2^32,客户端端口数最多2^16,也就是服务器单机最大TCP连接数为2^48

但是实际服务器肯定承载不了那么大的连接数主要受文件描述符(默认1024)和系统内存(TCP连接在内存有对应数据结构)限制。

如果服务器内存2GB,网卡千兆,能支持并发一万请求吗?硬件可行,但重点在于网络IO模型。

 

为了解决多用户C10K问题,我们分析一下多进程/多线程/IO多路复用三种解决方案。

(1)多进程

 

父进程主要负责监听socket,子进程主要负责已连接socket。(因为子进程会负责父进程的文件描述符,可以直接使用已连接socket和客户端通信)

(注意:在子进程退出的时候,内核里还会保留该进程的一些信息,也会占用内存,不做好回收功能,就会变成僵尸进程。随着僵尸进程越来越多,就会耗尽我们的系统资源。子进程退出后回收资源,分别是wait和waitpid)

每产生一个进程,都会占据一定的系统资源,而且进程间上下文切换会降低性能。进程间的上下文切换不仅包含虚拟内存、栈、全局变量等用户资源,还包括堆、栈、寄存器等内核空间资源。

(2)多线程

 

 单进程运行多个线程,同进程的线程共享进程的部分资源,比如文件描述符列表、进程空间、代码、全局数据、堆、共享库等等。

这些共享资源在上下文切换的时候不需要切换,只需要切换栈、寄存器等不共享的数据。

服务器和客户端TCP完成连接后,通过pthread_create()函数创建线程,然后将已连接socket的文件描述符传递给线程函数,接着在线程里和客户端进行通信,从而达到并发处理的目的。

也得创建销毁线程,麻烦。可以使用线程池。提前创建若干个线程,当新连接建立的时候,将已连接socket放到队列,线程池的线程负责从队列取出已连接socket进程处理。

(3)IO多路复用技术

只使用一个进程来维护多个socket。处理每个请求的事件耗时控制在1毫秒,1秒内就可以处理上千个请求。把时间拉长来看,其实就是在一定时间内一个进程模拟并发时间。

select/poll/epoll就是内核提供给用户态的多路复用系统调用,一个用户进程可以通过这个调用从内核中获取多个事件。

如何获取网络事件?先把所有文件描述符传给内核,然后内核检查是否产生事件的连接,然后在用户态中处理这些连接对应的请求就可以。

select:把已经连接的socket放到文件描述符集合,然后调用该函数将文件描述符集合拷贝到内核,让内核来遍历检查是否有事件产生。当检查到有事件产生,就对这个socket做标志,接着再把整个文件描述符集合拷贝回用户态,然后用户态再遍历处理。

需要两次遍历,两次拷贝。select使用固定bitsmap,表示文件描述符集合,而且所支持的文件描述符的个数有限制,FD_SETSIZE。

poll不再使用bitsmap来存储文件描述符,而是用动态数组,以链表形式组织,突破了函数设置的文件描述符个数限制,但是还是会受到系统文件描述符限制。

相同点就在于二者都是用线性结构存储进程关注的文件描述符集合,因此都需要两次遍历和两次拷贝。

 epoll就不一样了。epoll_create在内核创建epoll对象。

epoll在内核里使用红黑树来跟踪文件描述符,把需要监控的所有socket通过epoll_ctl()函数挂到红黑树,因为在内核就不需要进行整个文件描述符集合的拷贝操作,减少了内核和用户空间大量的数据拷贝和内存分配。

epoll使用事件驱动机制,内核里维护了一个链表来记录就绪事件。当有某个socket发生事件,就会通过回调函数把它加入就绪事件列表中。即使用epoll_wait()函数返回有事件发生的文件描述符。调用epoll_wait返回之后将数据从内核拷贝到用户空间。

对于epoll_wait部分的处理,epoll支持两种不同的事件触发模式,分别是边缘触发ET和水平触发LT。

其中,水平触发就是只要出现满足事件的条件,比如内核有数据需要读,就会一直把这个事件传给用户。当内核通知用户那个文件描述符可读写,接下来还会继续通知。所以用户在收到通知之后,没必要一次执行尽可能多的操作。

边缘触发就是只有第一次有数据需要读才会传递这个事件给用户。内核只会通知用户一次,所以用户需要在收到通知后尽可能读写数据,以免错失机会。

因此,我们会循环从文件描述符读写数据,如果文件描述符是阻塞的,没有数据可读的时候,进程就会阻塞在读写函数,无法向下执行。

因此,ET模式一般和非阻塞IO搭配,程序一直执行IO操作,直到系统调用read或write返回错误,比如EAGAIN或EWOULBLOCK。

ET模式相比于LT模式,可以减少epoll_wait的系统调用次数。因为系统调用也是有上下文切换的开销。

重点:多路复用API返回的事件并不一定是可读写的,如果使用阻塞IO,那么在调用read和write的时候就会发生程序阻塞,因此最好搭配非阻塞IO。

 

标签:socket,网卡,描述符,内核,缓冲区,网络系统,拷贝,操作系统
来源: https://www.cnblogs.com/Candy003/p/15957454.html