sendfile
-
上次说错了,上次说的是3次拷贝,4次上下文切换
-
理论只有2次上下文切换,到底有几次拷贝?需要看硬件的支持程度,如果不支持,从文件读取缓冲区到套接字发送缓冲区中间需要cpu干预一次,就是3次拷贝,如果支持,网卡设备支持直接从文件读取缓冲区读取数据,则只有两次
-
slice和sendfile最大的区别
- slice只需要两次拷贝,但是不需要硬件支持,网卡直接从文件读取缓冲区获取数据
java生态圈
- kafaka持久化
- 对磁盘随机写入效率低,顺序写入效率高,可以达到300m/s的速度
Linux 网络 IO 模型
-
同步和异步,阻塞和非阻塞
-
同步和异步关注的是调用方是否主动获取结果
同步是调用方调用后,主动去获取结果
异步是调用方调用完后,通过回调的方法获取结果,而不是主动获取结果
-
阻塞和非阻塞关注的是在结果返回前,调用方的状态
阻塞是结果返回前,当前线程挂起,什么都不做,返回结果为止
非阻塞在结果返回前,线程去做其他的事,等到某一个时候,再去拿结果
-
-
同步阻塞是最常见的
-
同步非阻塞可以理解成轮询模式,不断的去询问是否有结果了,询问的过程中也可以去做其他的事情,一定是要调用方自己去问被调用方
-
异步阻塞,线程池submit提交任务后,返回一个future,通过future.get()在下面等着,这种模式很傻,极少用
-
异步非阻塞,
linux下的五种i/o模型
- 1.阻塞io(blocking io)
- 2.非阻塞io(nonblocking io)
- 3.io复用(select、poll和epoll)
- 4.信号驱动io(signal drivem io(SIGIO))
- 5.异步io(asynchronous io)
- 前四种都是同步,只有5是异步,linux的异步是伪异步,真正实现异步的只有windows的iocp,linux开发组认为异步编程带来的性能并不明显
1.阻塞式io
- 进程阻塞与recvfrom的调用,应用进程一直阻塞,发生内核调用,一直阻塞到内核收到数据,并从内核空间拷贝到用户空间,拷贝结束,才算完成
- 进程会一直阻塞,直到数据拷贝完成
2.非阻塞 IO 模型
- 我们把一个 SOCKET 接口设置为非阻塞就是告诉内核,当所请求的 I/O 操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的 I/O 操作函数将通过循环不断的测试数据是否已经准备好(轮询),如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会 大量的占用 CPU 的时间。上述模型绝不被推荐。
3.IO 复用模型
-
将阻塞调用中的请求数据和接受数据拆成两个,由select检查当前进程是否有数据过来,有数据过来后,当前进程会收到相关事件的通知,再用实际读取网络数据的系统调用再去读取真实的数据
-
主要是 select 和 epoll 两个系统调用;对一个 IO 端口,两次调用,两次返回,比阻塞 IO 并没有什么优越性;关键是能实现同时对多个 IO 端口进行监听;
-
I/O 复用模型会用到 select、poll、epoll 函数,这几个函数也会使进程阻塞,但是和阻
塞 I/O 所不同的的,这两个函数可以同时阻塞多个 I/O 操作。而且可以同时对多个读操作,
多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。
-
去阻塞io相比?
-
一次读写阻塞io更高
-
为什么大量使用io复用
因为io复用最大的好处是可以让一个线程同时监听多个socket,因为线程资源是更昂贵的,综合考虑,所以使用io复用
-
-
select和recvfrom都是阻塞式的,所以io复用也是阻塞式的
4.信号驱动 IO
-
首先我们允许套接口进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻
塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函
数处理数据。
5.异步 IO 模型
-
当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完
成后,通过状态、通知和回调来通知调用者的输入输出操作
5种io的比较
- 前四种的第一阶段处理不同,等待数据不同,阻塞io一直等着,非阻塞io一直去检查,io多路复用单独提供了一个检查相关的系统调用,信号驱动和异步io都没有这个过程
- 第二阶段处理不同,都会阻塞在这里,都需要等待数据从内核空间拷贝到用户空间
Linux 下的阻塞网络编程
客户端
tcp_client.c
-
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #define PORT 8888 /*侦听端口地址*/ int main(int argc, char *argv[]) { int s; /*s为socket描述符*/ struct sockaddr_in server_addr; /*服务器地址结构*/ s = socket(AF_INET, SOCK_STREAM, 0); /*建立一个流式套接字 */ if(s < 0){ /*出错*/ printf("socket error\n"); return -1; } /*设置服务器地址*/ bzero(&server_addr, sizeof(server_addr)); /*清零*/ server_addr.sin_family = AF_INET; /*协议族*/ server_addr.sin_addr.s_addr = htonl(INADDR_ANY); /*本地地址*/ server_addr.sin_port = htons(PORT); /*服务器端口*/ /*将用户输入的字符串类型的IP地址转为整型*/ inet_pton(AF_INET, argv[1], &server_addr.sin_addr); /*连接服务器*/ connect(s, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)); process_conn_client(s); /*客户端处理过程*/ close(s); /*关闭连接*/ return 0; }
tcp_process.c
-
#include <stdio.h> #include <string.h> /*客户端的处理过程*/ void process_conn_client(int s) { ssize_t size = 0; char buffer[1024]; /*数据的缓冲区*/ for(;;){ /*循环处理过程*/ /*从标准输入中读取数据放到缓冲区buffer中*/ size = read(0, buffer, 1024); if(size > 0){ /*读到数据*/ write(s, buffer, size); /*发送给服务器*/ size = read(s, buffer, 1024); /*从服务器读取数据*/ write(1, buffer, size); /*写到标准输出*/ } } } /*服务器对客户端的处理*/ void process_conn_server(int s) { ssize_t size = 0; char buffer[1024]; /*数据的缓冲区*/ for(;;){ /*循环处理过程*/ size = read(s, buffer, 1024); /*从套接字中读取数据放到缓冲区buffer中*/ if(size == 0){ /*没有数据*/ return; } /*构建响应字符,为接收到客户端字节的数量*/ sprintf(buffer, "%d bytes altogether\n", size); write(s, buffer, strlen(buffer)+1);/*发给客户端*/ } }
tcp_server.c
-
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #define PORT 8888 /*侦听端口地址*/ #define BACKLOG 2 /*侦听队列长度*/ int main(int argc, char *argv[]) { int ss,sc; /*ss为服务器的socket描述符,sc为客户端的socket描述符*/ struct sockaddr_in server_addr; /*服务器地址结构*/ struct sockaddr_in client_addr; /*客户端地址结构*/ int err; /*返回值*/ pid_t pid; /*分叉的进行ID*/ /*建立一个流式套接字*/ ss = socket(AF_INET, SOCK_STREAM, 0); if(ss < 0){ /*出错*/ printf("socket error\n"); return -1; } /*设置服务器地址*/ bzero(&server_addr, sizeof(server_addr)); /*清零*/ server_addr.sin_family = AF_INET; /*协议族*/ server_addr.sin_addr.s_addr = htonl(INADDR_ANY); /*本地地址*/ server_addr.sin_port = htons(PORT); /*服务器端口*/ /*绑定地址结构到套接字描述符*/ err = bind(ss, (struct sockaddr*)&server_addr, sizeof(server_addr)); if(err < 0){/*出错*/ printf("bind error\n"); return -1; } /*设置侦听*/ err = listen(ss, BACKLOG); if(err < 0){ /*出错*/ printf("listen error\n"); return -1; } /*主循环过程*/ for(;;) { socklen_t addrlen = sizeof(struct sockaddr); sc = accept(ss, (struct sockaddr*)&client_addr, &addrlen); /*接收客户端连接*/ if(sc < 0){ /*出错*/ continue; /*结束本次循环*/ } /*建立一个新的进程处理到来的连接*/ pid = fork(); /*分叉进程*/ if( pid == 0 ){ /*子进程中*/ process_conn_server(sc); /*处理连接*/ close(ss); /*在子进程中关闭服务器的侦听*/ }else{ close(sc); /*在父进程中关闭客户端的连接*/ } } }
-
相比于jdk多了一个侦听的过程,之前java写的bio通讯,其中的bind方法就包含了绑定和侦听两个内容
-
jdk有一个单独的serverSocket,而c代码中没有这个概念,是jdk根据服务端不同的通讯行为抽象出的新概念
-
if( pid == 0 ){ /*子进程中*/ process_conn_server(sc); /*处理连接*/ close(ss); /*在子进程中关闭服务器的侦听*/ }else{ close(sc); /*在父进程中关闭客户端的连接*/ }
服务端的socket,即ss不会和服务器有建立连接的过程,真正建立连接的是sc
拓展
- 目前c++的DPDK网络通信是最快的,绕过了很多操作系统的限制
从 Linux 代码结构看网络通信
-
drivers目录和net目录
-
tcp协议主要分布在net目录和kernel目录
- net目录下ipv4和ipv6
-
引用API层
- socket方法 --> net/socket.c 又会调用 net/sysctl_net.c
-
协议层
- net/ipv*
-
接口层
- net/loop back
- drivers/以太网卡驱动
实际程序调用过程
-
程序 --> 应用API层(socket) ^ | 应用API层队列 应用层API层 --函数调用--> 协议层(TCP、IP、UDP等等) <--软中断,接口层产生 ^ | 协议层队列 协议层 --启动输出(接口层队列)--> 接口层 <--硬中断,网络设备产生
-
为什么接受到数据后还要流经协议层?
- 需要解包,解完包后才是真正应用层要处理的数据
-
什么是软中断、什么是硬中断?
中断、上半部、下半部
-
内核和设备驱动是通过中断的方式来处理的。所谓中断,可以理解为当设备上有数据到达的时候,会给 CPU 的相关引脚上触发一个电压变化,以通知 CPU 来处理数据。
-
计算机执行程序时,会有优先级的需求。比如,当计算机收到断电信号时(电容可以保存少许电量,供 CPU 运行很短的一小段时间),它应立即去保存数据,保存数据的程序具 有较高的优先级。
-
一般而言,由硬件产生的信号需要 cpu 立马做出回应(不然数据可能就丢失),所以它 的优先级很高。cpu 理应中断掉正在执行的程序,去做出响应;当 cpu 完成对硬件的响应后, 再重新执行用户程序。中断的过程如下图,和函数调用差不多。只不过函数调用是事先定好位置,而中断的位置由“信号”决定。
-
如果中断处理的时间很久?
1.会把中断分成两个部分,中断上半部和中断下半部(linux2.4,2.4以前全是硬中断,之后分成上下半部)
因此 Linux 中断处理函数是分上半部和下半部的。上半部是只进行最简单的工作,快速处理然后释放 CPU,接着 CPU 就可以允许其它中断进来。剩下将绝大部分的工作都放到下半 部中,可以慢慢从容处理。2.4 以后的内核版本采用的下半部实现方式是软中断,由ksoftirqd由内核线程全权处理。和硬中断不同的是,硬中断是通过给 CPU 物理引脚施加电压变化,而软中断是通过给内存中的一个变量的二进制值以通知软中断处理程序。
2.中断上半部处理硬中断的过程,只做最简单中断最简单的相关处理,然后马上释放CPU
3.与中断相关不是特别紧急的任务,由cpu慢慢的从容的处理
-
以键盘为例,当用户按下键盘某个按键时,键盘会给 cpu 的中断引脚发出一个高电平。cpu 能够捕获这个信号,然后执行键盘中断程序
有的时候键盘输入很卡,输入了没显示,但是过一会又显示出来了,就是因为分成了上下部分
-
Linux 网络包接收过程
- 具体过程
- 1.准备过程
- 2.收包
- CPU、硬件控制器、网卡、总线、内存
宏观具体过程
- 1.数据从外部到达网卡
- 2.网卡把数据DMA到内存中
- 3.网卡发出一个硬中断通知CPU
- 4.CPU响应硬中断,发出软中断
- 5.kssoftirqd进程处理软中断,调用网卡驱动程序,进行轮询,在网卡专属的内存里面可能会有很多数据,一次函数调用会读取若干个包,进程反复调用驱动程序
- 6.反复收包,收完后,保存为skb的数据结构,skb专门负责数据在层与层之间进行流动
- 7.协议层处理skb
- 8.协议层拆分完后,处理完了,拆分成了应用能够感知的业务数据后,放到socket的接受队列
- 9.操作系统就会通知我们的程序进行相关处理,唤醒被阻塞的用户程序(阻塞式io)
准备工作
准备工作1
- 1.操作在启动的时候一定要创建kssoftirqd进程,进程个数等于CPU核心数
- 2.网络子系统的初始化,在net/core/dev.c文件中,net_dev_init方法,创建每个CPU独有的softnet_data的数据结构
- 3.协议栈的注册,linux在协议注册时不是写死的,而是采用注册的模式,在net/ipv4/af_inet.c中,在init_init()方法中进行注册,第一个是inet_proto(tcp、udp、icmp的报文怎么处理),ptyte_base(ipv4、ipv6怎么处理),这两个数据结构存储的是处理函数的相关地址
- 4.网卡驱动初始化和网卡的启动
准备工作2–创建socket
- net/socket.c
- 可以看到很多与socket相关的函数
- bind方法,_sys_bind
- 具体过程
- 1.和linux的文件系统挂钩,会诞生一个socket文件系统
- 2.初始化socket
- 3.socket缓冲区、队列都创建出来
步骤4硬中断具体做了什么
- 修改softnet_data,告诉cpu网卡现在有数据,需要处理了
JDK 中的 BIO 实现分析
-
默认的实现类是SocketImpl,ServerSocket 和 Socket 只是一个门面模式。
-
SocketOptions
-
SocketImpl
-
AbstractPlainSocketImpl,主要是套接字一些默认的相关实现
-
PlainSocketImpl,本质上是一个代理类
-
SocksSocketImpl,PlainSocketImpl派生出来的
-
PlainSocketImpl的实际读写还是AbstractPlainSocketImpl进行的
例如accept方法,又去调用了AbstractPlainSocketImpl方法,而PlainSocketImpl又是AbstractPlainSocketImpl的实现类,这是windows下看到的,但是在linux下看到的是完全不同的(jdk_linux_src),在linux下基本都是原生native方法
-
-
DualStackPlainSocketImpl和TwoStackPlainSocketImpl都是windows下面使用的,只是版本不同