当一个网络帧到达网卡后,网卡会通过 DMA 方式,把这个网络包放到收包队列中;然后通过硬中断,告诉中断处理程序已经收到了网络包
(网卡收到网线传来的数据,通过硬件电路的传输,将数据写入到内存中的某个地址上
当网卡把数据写入到内存后,网卡向 CPU 发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。)
(
内核分配一个主内存地址段(DMA缓冲区),网卡设备可以在DMA缓冲区中读写数据
当来了一个网络包,网卡将网络包写入DMA缓冲区,写完后通知CPU产生硬中断
硬中断处理程序锁定当前DMA缓冲区,然后将网络包拷贝到另一块内存区,清空并解锁当前DMA缓冲区,然后通知软中断去处理网络包。
)
从操作系统进程调度的角度来看数据接收。阻塞是进程调度的关键一环,指的是进程在等待某事件(如接收到网络数据)发生之前的等待状态,recv、select 和 epoll 都是阻塞方法
网卡收发网络包时,通过 DMA 方式交互的环形缓冲区;
网卡中断处理程序为网络帧分配的,内核数据结构 sk_buff 缓冲区;
应用程序通过套接字接口,与网络协议栈交互时的套接字缓冲区。
这些缓冲区都处于内核管理的内存中
其中,环形缓冲区,由于需要 DMA 与网卡交互,理应属于网卡设备驱动的范围。
sk_buff 缓冲区,是一个维护网络帧结构的双向链表,链表中的每一个元素都是一个网络帧(Packet)。虽然 TCP/IP 协议栈分了好几层,但上下不同层之间的传递,实际上只需要操作这个数据结构中的指针,而无需进行数据复制。
套接字缓冲区,则允许应用程序,给每个套接字配置不同大小的接收或发送缓冲区。应用程序发送数据,实际上就是将数据写入缓冲区;而接收数据,其实就是从缓冲区中读取。至于缓冲区中数据的进一步处理,则由传输层的 TCP 或 UDP 协议来完成。
其中的软中断处理,就有专门的内核线程 ksoftirqd。每个 CPU 都会绑定一个 ksoftirqd 内核线程,比如, 2 个 CPU 时,就会有 ksoftirqd/0 和 ksoftirqd/1 这两个内核线程。
操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态。运行状态是进程获得 CPU 使用权,正在执行代码的状态;等待状态是阻塞状态,比如上述程序运行到 recv 时,程序会从运行状态变为等待状态,接收到数据后又变回运行状态。操作系统会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。
//创建socket
int s = socket(AF_INET, SOCK_STREAM, 0);
//绑定
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据
recv(c, ...);
//将数据打印出来
printf(...)
因为一个 socket 对应着一个端口号,而网络数据包中包含了 ip 和端口的信息,内核可以通过端口号找到对应的 socket
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...);
int fds[] = 存放需要监听的socket;
while(1){
int n = select(..., fds, ...)
for(int i=0; i < fds.count; i++){
if(FD_ISSET(fds[i], ...)){
//fds[i]的数据处理
}
}}
其一,每次调用 select 都需要将进程加入到所有监视 socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个 fds 列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定 select 的最大监视数量,默认只能监视 1024 个 socket。
其二,进程被唤醒后,程序并不知道哪些 socket 收到数据,还需要遍历一次。
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...)
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
while(1){
int n = epoll_wait(...)
for(接收到数据的socket){
//处理
}
}
select 低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。如下图所示,每次调用 select 都需要这两步操作,然而大多数应用场景中,需要监视的 socket 相对固定,并不需要每次都修改。epoll 将这两个操作分开,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见地,效率就能得到提升。
select 低效的一个原因在于程序不知道哪些 socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的 socket,就能避免遍历
当 socket 收到数据后,中断程序会给 eventpoll 的“就绪列表”添加 socket 引用。如下图展示的是 sock2 和 sock3 收到数据后,中断程序让 rdlist 引用这两个 socket。
某个进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象(也就是程序中 epfd 所代表的对象)。eventpoll 对象也是文件系统中的一员,和 socket 一样,它也会有等待队列。
当 socket 收到数据后,中断程序会操作 eventpoll 对象,而不是直接操作进程
当 socket 收到数据后,中断程序会给 eventpoll 的“就绪列表”添加 socket 引用
当程序执行到 epoll_wait 时,如果 rdlist 已经引用了 socket,那么 epoll_wait 直接返回,如果 rdlist 为空,阻塞进程,如下图
epoll 使用双向链表来实现就绪队列(rdllist)
用红黑树保存监视的socket,根据端口,能快速找到对应的socket
eventpoll 对象相当于 socket 和进程之间的中介,socket 的数据接收并不直接影响进程,而是通过改变 eventpoll 的就绪列表来改变进程状态。
当程序执行到 epoll_wait 时,如果 rdlist 已经引用了 socket,那么 epoll_wait 直接返回,如果 rdlist 为空,阻塞进程。
当 socket 接收到数据,中断程序一方面修改 rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态(如下图)。也因为 rdlist 的存在,进程 A 可以知道哪些 socket 发生了变化
socket套接字是一种有相应描述符的打开文件(open—write/read—close)
socket 维护两个队列。一个是已经建立了连接的队列,这时候连接三次握手已经完毕,处于 established 状态;一个是还没有完全建立连接的队列,这个时候三次握手还没完成,处于 syn_rcvd 的状态
socket 在 Linux 中就是以文件的形式存在的,即对应一个文件描述符
每一个进程都有一个数据结构 task_struct,里面指向一个文件描述符数组,来列出这个进程打开的所有文件的文件描述符。文件描述符是一个整数,是这个数组的下标
socket结构里,主要的是两个队列,一个是发送队列,一个是接收队列。在这两个队列里面保存的是一个缓存 sk_buff。这个缓存里面能够看到完整的包的结构
socket上定义了几个IO事件:状态改变事件、有数据可读事件、有发送缓存可写事件、有IO错误事件。对于这些事件,socket中分别定义了相应的事件处理函数,也称回调函数。
Socket I/O事件的处理过程中,要使用到sock上的两个队列:等待队列和异步通知队列,这两个队列中都保存着等待该Socket I/O事件的进程。
进程 A 运行到了 epoll_wait 语句,内核会将进程 A 放入 eventpoll 的等待队列wq中,阻塞进程,eventpoll红黑树rbr维护监听的socket,当 socket 接收到数据(ip:port),中断程序快速查找socket,并放入eventpoll就绪队列rdlist,唤醒 eventpoll 等待队列中的进程
1.1.创建套接字,协议、类型
socket()
1.2.绑定地址、端口
bind()
1.3.声明该套接字用于监听
mark a connection-mode socket, specified by the socket argument, as accepting connections.
默认的socket是主动性套接字,用于客户端,listen函数告诉内核该socket是用于监听。
listen()
1.4.监听套接字直到连接到来,每次接受连接请求会生成已连接描述符,用于客户端通信
accept()
Accept a connection with the accept() system call. This call typically blocks until a client connects with the server.
如果已经建立了连接的队列有新连接,则accept返回新连接的socket,否则一直阻塞
2.1.socket()
2.2.connect()
https://my.oschina.net/editorial-story/blog/3052308