IO 模型知多少 | 代码篇

引言

之前的一篇介绍IO 模型的文章IO 模型知多少 -- 理论篇比较偏理论,很多同学反应不是很好理解。这一篇咱们换一个角度,从代码角度来分析一下。

socket 编程基础

开始之前,我们先来梳理一下,需要提前了解的几个概念:

socket: 直译为“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。我们把插头插到插座上就能从电网获得电力供应,同样,应用程序为了与远程计算机进行数据传输,需要连接到因特网,而 socket 就是用来连接到因特网的工具。

另外还需要知道的是,socket 编程的基本流程。

2. 同步阻塞IO

先回顾下概念:阻塞IO是指,应用进程中线程在发起IO调用后至内核执行IO操作返回结果之前,若发起系统调用的线程一直处于等待状态,则此次IO操作为阻塞IO。

public static void Start()
{//1. 创建Tcp Socket对象var serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);var ipEndpoint = new IPEndPoint(IPAddress.Loopback, 5001);//2. 绑定Ip端口serverSocket.Bind(ipEndpoint);//3. 开启监听,指定最大连接数serverSocket.Listen(10);Console.WriteLine($"服务端已启动({ipEndpoint})-等待连接...");while (true){//4. 等待客户端连接var clientSocket = serverSocket.Accept();//阻塞Console.WriteLine($"{clientSocket.RemoteEndPoint}-已连接");Span<byte> buffer = new Span<byte>(new byte[512]);Console.WriteLine($"{clientSocket.RemoteEndPoint}-开始接收数据...");int readLength = clientSocket.Receive(buffer);//阻塞var msg = Encoding.UTF8.GetString(buffer.ToArray(), 0, readLength);Console.WriteLine($"{clientSocket.RemoteEndPoint}-接收数据:{msg}");var sendBuffer = Encoding.UTF8.GetBytes($"received:{msg}");clientSocket.Send(sendBuffer);}
}

代码很简单,直接看注释就OK了,运行结果如上图所示,但有几个问题点需要着重说明下:

  1. 等待连接处 serverSocket.Accept(),线程阻塞!

  2. 接收数据处 clientSocket.Receive(buffer),线程阻塞!

会导致什么问题呢:

  1. 只有一次数据读取完成后,才可以接受下一个连接请求

  2. 一个连接,只能接收一次数据

同步非阻塞IO

看完,你可能会说,这两个问题很好解决啊,创建一个新线程去接收数据就是了。于是就有了下面的代码改进。

public static void Start2()
{//1. 创建Tcp Socket对象var serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);var ipEndpoint = new IPEndPoint(IPAddress.Loopback, 5001);//2. 绑定Ip端口serverSocket.Bind(ipEndpoint);//3. 开启监听,指定最大连接数serverSocket.Listen(10);Console.WriteLine($"服务端已启动({ipEndpoint})-等待连接...");while (true){//4. 等待客户端连接var clientSocket = serverSocket.Accept();//阻塞Task.Run(() => ReceiveData(clientSocket));}
}
private static void ReceiveData(Socket clientSocket)
{Console.WriteLine($"{clientSocket.RemoteEndPoint}-已连接");Span<byte> buffer = new Span<byte>(new byte[512]);while (true){if (clientSocket.Available == 0) continue;Console.WriteLine($"{clientSocket.RemoteEndPoint}-开始接收数据...");int readLength = clientSocket.Receive(buffer);//阻塞var msg = Encoding.UTF8.GetString(buffer.ToArray(), 0, readLength);Console.WriteLine($"{clientSocket.RemoteEndPoint}-接收数据:{msg}");var sendBuffer = Encoding.UTF8.GetBytes($"received:{msg}");clientSocket.Send(sendBuffer);}
}

是的,多线程解决了上述的问题,但如果你观察以上动图后,你应该能发现个问题:才建立4个客户端连接,CPU的占用率就开始直线上升了。

而这个问题的本质就是,服务端的IO模型为阻塞IO模型,为了解决阻塞导致的问题,采用重复轮询,导致无效的系统调用,从而导致CPU持续走高。

IO多路复用

既然知道原因所在,咱们就来予以改造。适用异步方式来处理连接、接收和发送数据。

public static class NioServer
{private static ManualResetEvent _acceptEvent = new ManualResetEvent(true);private static ManualResetEvent _readEvent = new ManualResetEvent(true);public static void Start(){//1. 创建Tcp Socket对象var serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);// serverSocket.Blocking = false;//设置为非阻塞var ipEndpoint = new IPEndPoint(IPAddress.Loopback, 5001);//2. 绑定Ip端口serverSocket.Bind(ipEndpoint);//3. 开启监听,指定最大连接数serverSocket.Listen(10);Console.WriteLine($"服务端已启动({ipEndpoint})-等待连接...");while (true){_acceptEvent.Reset();//重置信号量serverSocket.BeginAccept(OnClientConnected, serverSocket);_acceptEvent.WaitOne();//阻塞}}private static void OnClientConnected(IAsyncResult ar){_acceptEvent.Set();//当有客户端连接进来后,则释放信号量var serverSocket = ar.AsyncState as Socket;Debug.Assert(serverSocket != null, nameof(serverSocket) + " != null");var clientSocket = serverSocket.EndAccept(ar);Console.WriteLine($"{clientSocket.RemoteEndPoint}-已连接");while (true){_readEvent.Reset();//重置信号量var stateObj = new StateObject { ClientSocket = clientSocket };clientSocket.BeginReceive(stateObj.Buffer, 0, stateObj.Buffer.Length, SocketFlags.None, OnMessageReceived, stateObj);_readEvent.WaitOne();//阻塞等待}}private static void OnMessageReceived(IAsyncResult ar){var state = ar.AsyncState as StateObject;Debug.Assert(state != null, nameof(state) + " != null");var receiveLength = state.ClientSocket.EndReceive(ar);if (receiveLength > 0){var msg = Encoding.UTF8.GetString(state.Buffer, 0, receiveLength);Console.WriteLine($"{state.ClientSocket.RemoteEndPoint}-接收数据:{msg}");var sendBuffer = Encoding.UTF8.GetBytes($"received:{msg}");state.ClientSocket.BeginSend(sendBuffer, 0, sendBuffer.Length, SocketFlags.None,SendMessage, state.ClientSocket);}}private static void SendMessage(IAsyncResult ar){var clientSocket = ar.AsyncState as Socket;Debug.Assert(clientSocket != null, nameof(clientSocket) + " != null");clientSocket.EndSend(ar);_readEvent.Set(); //发送完毕后,释放信号量}
}
public class StateObject
{// Client  socket.  public Socket ClientSocket = null;// Size of receive buffer.  public const int BufferSize = 1024;// Receive buffer.  public byte[] Buffer = new byte[BufferSize];
}

首先来看运行结果,从下图可以看到,除了建立连接时CPU出现抖动外,在消息接收和发送阶段,CPU占有率趋于平缓,且占用率低。

分析代码后我们发现:

  1. CPU使用率是下来了,但代码复杂度上升了。

  2. 使用异步接口处理客户端连接: BeginAccept和 EndAccept

  3. 使用异步接口接收数据: BeginReceive和 EndReceive

  4. 使用异步接口发送数据: BeginSend和 EndSend

  5. 使用 ManualResetEvent进行线程同步,避免线程空转

那你可能好奇,以上模型是何种IO多路复用模型呢? 好问题,我们来一探究竟。

验证I/O模型

要想验证应用使用的何种IO模型,只需要确定应用运行时发起了哪些系统调用即可。对于Linux系统来说,我们可以借助 strace命令来跟踪指定应用发起的系统调用和信号。

验证同步阻塞I/O发起的系统调用

可以使用VSCode Remote 连接到自己的Linux系统上,然后新建项目 Io.Demo,以上面非阻塞IO的代码进行测试,执行以下启动跟踪命令:

shengjie@ubuntu:~/coding/dotnet$ ls
Io.Demo
shengjie@ubuntu:~/coding/dotnet$ strace -ff -o Io.Demo/strace/io dotnet run --project Io.Demo/
Press any key to start!
服务端已启动(127.0.0.1:5001)-等待连接...
127.0.0.1:36876-已连接
127.0.0.1:36876-开始接收数据...
127.0.0.1:36876-接收数据:1

另起命令行,执行 nc localhost5001模拟客户端连接。

shengjie@ubuntu:~/coding/dotnet/Io.Demo$ nc localhost 5001
1
received:1

使用 netstat命令查看建立的连接。

shengjie@ubuntu:/proc/3763$ netstat -natp | grep 5001
(Not all processes could be identified, non-owned process infowill not be shown, you would have to be root to see it all.)
tcp        0      0 127.0.0.1:5001          0.0.0.0:*               LISTEN      3763/Io.Demo        
tcp        0      0 127.0.0.1:36920         127.0.0.1:5001          ESTABLISHED 3798/nc             
tcp        0      0 127.0.0.1:5001          127.0.0.1:36920         ESTABLISHED 3763/Io.Demo 

`

另起命令行,执行 ps-h|grep dotnet 抓取进程Id。

shengjie@ubuntu:~/coding/dotnet/Io.Demo$ ps -h | grep dotnet3694 pts/1    S+     0:11 strace -ff -o Io.Demo/strace/io dotnet run --project Io.Demo/3696 pts/1    Sl+    0:01 dotnet run --project Io.Demo/3763 pts/1    Sl+    0:00 /home/shengjie/coding/dotnet/Io.Demo/bin/Debug/netcoreapp3.0/Io.Demo3779 pts/2    S+     0:00 grep --color=auto dotnet
shengjie@ubuntu:~/coding/dotnet$ ls Io.Demo/strace/ # 查看生成的系统调用文件
io.3696  io.3702  io.3708  io.3714  io.3720  io.3726  io.3732  io.3738  io.3744  io.3750  io.3766  io.3772  io.3782  io.3827
io.3697  io.3703  io.3709  io.3715  io.3721  io.3727  io.3733  io.3739  io.3745  io.3751  io.3767  io.3773  io.3786  io.3828
io.3698  io.3704  io.3710  io.3716  io.3722  io.3728  io.3734  io.3740  io.3746  io.3752  io.3768  io.3774  io.3787
io.3699  io.3705  io.3711  io.3717  io.3723  io.3729  io.3735  io.3741  io.3747  io.3763  io.3769  io.3777  io.3797
io.3700  io.3706  io.3712  io.3718  io.3724  io.3730  io.3736  io.3742  io.3748  io.3764  io.3770  io.3780  io.3799
io.3701  io.3707  io.3713  io.3719  io.3725  io.3731  io.3737  io.3743  io.3749  io.3765  io.3771  io.3781  io.3800

有上可知,进程Id为3763,依次执行以下命令可以查看该进程的线程和产生的文件描述符:

shengjie@ubuntu:~/coding/dotnet/Io.Demo$ cd /proc/3763 # 进入进程目录
shengjie@ubuntu:/proc/3763$ ls
attr        cmdline          environ  io         mem         ns             pagemap      sched      smaps_rollup  syscall        wchan
autogroup   comm             exe      limits     mountinfo   numa_maps      patch_state  schedstat  stack         task
auxv        coredump_filter  fd       loginuid   mounts      oom_adj        personality  sessionid  stat          timers
cgroup      cpuset           fdinfo   map_files  mountstats  oom_score      projid_map   setgroups  statm         timerslack_ns
clear_refs  cwd              gid_map  maps       net         oom_score_adj  root         smaps      status        uid_map
shengjie@ubuntu:/proc/3763$ ll task # 查看当前进程启动的线程
total 0
dr-xr-xr-x 9 shengjie shengjie 0 5月  10 16:36 ./
dr-xr-xr-x 9 shengjie shengjie 0 5月  10 16:34 ../
dr-xr-xr-x 7 shengjie shengjie 0 5月  10 16:36 3763/
dr-xr-xr-x 7 shengjie shengjie 0 5月  10 16:36 3765/
dr-xr-xr-x 7 shengjie shengjie 0 5月  10 16:36 3766/
dr-xr-xr-x 7 shengjie shengjie 0 5月  10 16:36 3767/
dr-xr-xr-x 7 shengjie shengjie 0 5月  10 16:36 3768/
dr-xr-xr-x 7 shengjie shengjie 0 5月  10 16:36 3769/
dr-xr-xr-x 7 shengjie shengjie 0 5月  10 16:36 3770/
shengjie@ubuntu:/proc/3763$ ll fd 查看当前进程系统调用产生的文件描述符
total 0
dr-x------ 2 shengjie shengjie  0 5月  10 16:36 ./
dr-xr-xr-x 9 shengjie shengjie  0 5月  10 16:34 ../
lrwx------ 1 shengjie shengjie 64 5月  10 16:37 0 -> /dev/pts/1
lrwx------ 1 shengjie shengjie 64 5月  10 16:37 1 -> /dev/pts/1
lrwx------ 1 shengjie shengjie 64 5月  10 16:37 10 -> 'socket:[44292]'
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 100 -> /dev/random
lrwx------ 1 shengjie shengjie 64 5月  10 16:37 11 -> 'socket:[41675]'
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 13 -> 'pipe:[45206]'
l-wx------ 1 shengjie shengjie 64 5月  10 16:37 14 -> 'pipe:[45206]'
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 15 -> /home/shengjie/coding/dotnet/Io.Demo/bin/Debug/netcoreapp3.0/Io.Demo.dll
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 16 -> /home/shengjie/coding/dotnet/Io.Demo/bin/Debug/netcoreapp3.0/Io.Demo.dll
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 17 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Runtime.dll
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 18 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Console.dll
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 19 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Threading.dll
lrwx------ 1 shengjie shengjie 64 5月  10 16:37 2 -> /dev/pts/1
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 20 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Runtime.Extensions.dll
lrwx------ 1 shengjie shengjie 64 5月  10 16:37 21 -> /dev/pts/1
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 22 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Text.Encoding.Extensions.dll
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 23 -> /dev/urandom
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 24 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Net.Sockets.dll
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 25 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Net.Primitives.dll
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 26 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/Microsoft.Win32.Primitives.dll
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 27 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Diagnostics.Tracing.dll
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 28 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Threading.Tasks.dll
lrwx------ 1 shengjie shengjie 64 5月  10 16:37 29 -> 'socket:[43429]'
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 3 -> 'pipe:[42148]'
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 30 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Threading.ThreadPool.dll
lrwx------ 1 shengjie shengjie 64 5月  10 16:37 31 -> 'socket:[42149]'
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 32 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Memory.dll
l-wx------ 1 shengjie shengjie 64 5月  10 16:37 4 -> 'pipe:[42148]'
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 42 -> /dev/urandom
lrwx------ 1 shengjie shengjie 64 5月  10 16:37 5 -> /dev/pts/1
lrwx------ 1 shengjie shengjie 64 5月  10 16:37 6 -> /dev/pts/1
lrwx------ 1 shengjie shengjie 64 5月  10 16:37 7 -> /dev/pts/1
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 9 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Private.CoreLib.dll
lr-x------ 1 shengjie shengjie 64 5月  10 16:37 99 -> /dev/urandom

从上面的输出来看,.NET Core控制台应用启动时启动了多个线程,并在10、11、29、31号文件描述符启动了socket监听。那哪一个文件描述符监听的是5001端口呢。

shengjie@ubuntu:~/coding/dotnet/Io.Demo$ cat /proc/net/tcp | grep 1389 # 查看5001端口号相关的tcp链接(0x1389 为5001十六进制)4: 0100007F:1389 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 43429 1 0000000000000000 100 0 0 10 0                     12: 0100007F:9038 0100007F:1389 01 00000000:00000000 00:00000000 00000000  1000        0 44343 1 0000000000000000 20 4 30 10 -1                    13: 0100007F:1389 0100007F:9038 01 00000000:00000000 00:00000000 00000000  1000        0 42149 1 0000000000000000 20 4 29 10 -1

从中可以看到inode为[43429]的socket监听在5001端口号,所以可以找到上面的输出行 lrwx------1shengjie shengjie645月1016:3729->'socket:[43429]',进而判断监听5001端口号socket对应的文件描述符为29。

当然,也可以从记录到 strace目录的日志文件找到线索。在文中我们已经提及,socket服务端编程的一般流程,都要经过socket->bind->accept->read->write流程。所以可以通过抓取关键字,查看相关系统调用。

shengjie@ubuntu:~/coding/dotnet/Io.Demo$ grep 'bind' strace/ -rn
strace/io.3696:4570:bind(10, {sa_family=AF_UNIX, sun_path="/tmp/dotnet-diagnostic-3696-327175-socket"}, 110) = 0
strace/io.3763:2241:bind(11, {sa_family=AF_UNIX, sun_path="/tmp/dotnet-diagnostic-3763-328365-socket"}, 110) = 0
strace/io.3763:2949:bind(29, {sa_family=AF_INET, sin_port=htons(5001), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
strace/io.3713:4634:bind(11, {sa_family=AF_UNIX, sun_path="/tmp/dotnet-diagnostic-3713-327405-socket"}, 110) = 0

从上可知,在主线程也就是 io.3763线程的系统调用文件中,将29号文件描述符与监听在 127.0.0.1:5001的socket进行了绑定。同时也明白了.NET Core自动建立的另外2个socket是与diagnostic相关。 接下来咱们重点看下3763号线程产生的系统调用。

shengjie@ubuntu:~/coding/dotnet/Io.Demo$ cd strace/
shengjie@ubuntu:~/coding/dotnet/Io.Demo/strace$ cat io.3763 # 仅截取相关片段
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_TCP) = 29
setsockopt(29, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(29, {sa_family=AF_INET, sin_port=htons(5001), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
listen(29, 10)       
write(21, "\346\234\215\345\212\241\347\253\257\345\267\262\345\220\257\345\212\250(127.0.0.1:500"..., 51) = 51
accept4(29, {sa_family=AF_INET, sin_port=htons(36920), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_CLOEXEC) = 31
write(21, "127.0.0.1:36920-\345\267\262\350\277\236\346\216\245\n", 26) = 26
write(21, "127.0.0.1:36920-\345\274\200\345\247\213\346\216\245\346\224\266\346\225\260\346"..., 38) = 38
recvmsg(31, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="1\n", iov_len=512}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, 0) = 2
write(21, "127.0.0.1:36920-\346\216\245\346\224\266\346\225\260\346\215\256\357\274\2321"..., 34) = 34
sendmsg(31, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="received:1\n", iov_len=11}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, 0) = 11
accept4(29, 0x7fecf001c978, [16], SOCK_CLOEXEC) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGWINCH {si_signo=SIGWINCH, si_code=SI_KERNEL} ---

从中我们可以发现几个关键的系统调用:socket、bind、listen、accept4、recvmsg、sendmsg 通过命令 man命令可以查看下 accept4和 recvmsg系统调用的相关说明:

shengjie@ubuntu:~/coding/dotnet/Io.Demo/strace$ man accept4
If no pending connections are present on the queue, and the socket is not marked as nonblocking, accept() blocks the caller until aconnection  is  present.
shengjie@ubuntu:~/coding/dotnet/Io.Demo/strace$ man recvmsg
If  no  messages are available at the socket, the receive calls wait for a message to arrive, unless the socket is nonblocking (see fcntl(2))

也就是说 accept4recvmsg是阻塞式系统调用。

验证I/O多路复用发起的系统调用

同样以上面I/O多路复用的代码进行验证,验证步骤类似:

shengjie@ubuntu:~/coding/dotnet$ strace -ff -o Io.Demo/strace2/io dotnet run --project Io.Demo/
Press any key to start!
服务端已启动(127.0.0.1:5001)-等待连接...
127.0.0.1:37098-已连接
127.0.0.1:37098-接收数据:1
127.0.0.1:37098-接收数据:2
shengjie@ubuntu:~/coding/dotnet/Io.Demo$ nc localhost 5001
1
received:1
2
received:2
shengjie@ubuntu:/proc/2449$ netstat -natp | grep 5001
(Not all processes could be identified, non-owned process infowill not be shown, you would have to be root to see it all.)
tcp        0      0 127.0.0.1:5001          0.0.0.0:*               LISTEN      2449/Io.Demo        
tcp        0      0 127.0.0.1:5001          127.0.0.1:56296         ESTABLISHED 2449/Io.Demo        
tcp        0      0 127.0.0.1:56296         127.0.0.1:5001          ESTABLISHED 2499/nc        
shengjie@ubuntu:~/coding/dotnet/Io.Demo$ ps -h | grep dotnet2400 pts/3    S+     0:10 strace -ff -o ./Io.Demo/strace2/io dotnet run --project Io.Demo/2402 pts/3    Sl+    0:01 dotnet run --project Io.Demo/2449 pts/3    Sl+    0:00 /home/shengjie/coding/dotnet/Io.Demo/bin/Debug/netcoreapp3.0/Io.Demo2516 pts/5    S+     0:00 grep --color=auto dotnet
shengjie@ubuntu:~/coding/dotnet/Io.Demo$ cd /proc/2449/
shengjie@ubuntu:/proc/2449$ ll task
total 0
dr-xr-xr-x 11 shengjie shengjie 0 5月  10 22:15 ./
dr-xr-xr-x  9 shengjie shengjie 0 5月  10 22:15 ../
dr-xr-xr-x  7 shengjie shengjie 0 5月  10 22:15 2449/
dr-xr-xr-x  7 shengjie shengjie 0 5月  10 22:15 2451/
dr-xr-xr-x  7 shengjie shengjie 0 5月  10 22:15 2452/
dr-xr-xr-x  7 shengjie shengjie 0 5月  10 22:15 2453/
dr-xr-xr-x  7 shengjie shengjie 0 5月  10 22:15 2454/
dr-xr-xr-x  7 shengjie shengjie 0 5月  10 22:15 2455/
dr-xr-xr-x  7 shengjie shengjie 0 5月  10 22:15 2456/
dr-xr-xr-x  7 shengjie shengjie 0 5月  10 22:15 2459/
dr-xr-xr-x  7 shengjie shengjie 0 5月  10 22:15 2462/
shengjie@ubuntu:/proc/2449$ ll fd
total 0
dr-x------ 2 shengjie shengjie  0 5月  10 22:15 ./
dr-xr-xr-x 9 shengjie shengjie  0 5月  10 22:15 ../
lrwx------ 1 shengjie shengjie 64 5月  10 22:16 0 -> /dev/pts/3
lrwx------ 1 shengjie shengjie 64 5月  10 22:16 1 -> /dev/pts/3
lrwx------ 1 shengjie shengjie 64 5月  10 22:16 10 -> 'socket:[35001]'
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 100 -> /dev/random
lrwx------ 1 shengjie shengjie 64 5月  10 22:16 11 -> 'socket:[34304]'
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 13 -> 'pipe:[31528]'
l-wx------ 1 shengjie shengjie 64 5月  10 22:16 14 -> 'pipe:[31528]'
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 15 -> /home/shengjie/coding/dotnet/Io.Demo/bin/Debug/netcoreapp3.0/Io.Demo.dll
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 16 -> /home/shengjie/coding/dotnet/Io.Demo/bin/Debug/netcoreapp3.0/Io.Demo.dll
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 17 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Runtime.dll
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 18 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Console.dll
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 19 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Threading.dll
lrwx------ 1 shengjie shengjie 64 5月  10 22:16 2 -> /dev/pts/3
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 20 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Runtime.Extensions.dll
lrwx------ 1 shengjie shengjie 64 5月  10 22:16 21 -> /dev/pts/3
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 22 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Text.Encoding.Extensions.dll
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 23 -> /dev/urandom
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 24 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Net.Sockets.dll
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 25 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Net.Primitives.dll
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 26 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/Microsoft.Win32.Primitives.dll
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 27 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Diagnostics.Tracing.dll
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 28 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Threading.Tasks.dll
lrwx------ 1 shengjie shengjie 64 5月  10 22:16 29 -> 'socket:[31529]'
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 3 -> 'pipe:[32055]'
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 30 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Threading.ThreadPool.dll
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 31 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Collections.Concurrent.dll
lrwx------ 1 shengjie shengjie 64 5月  10 22:16 32 -> 'anon_inode:[eventpoll]'
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 33 -> 'pipe:[32059]'
l-wx------ 1 shengjie shengjie 64 5月  10 22:16 34 -> 'pipe:[32059]'
lrwx------ 1 shengjie shengjie 64 5月  10 22:16 35 -> 'socket:[35017]'
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 36 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Memory.dll
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 37 -> /dev/urandom
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 38 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Diagnostics.Debug.dll
l-wx------ 1 shengjie shengjie 64 5月  10 22:16 4 -> 'pipe:[32055]'
lrwx------ 1 shengjie shengjie 64 5月  10 22:16 5 -> /dev/pts/3
lrwx------ 1 shengjie shengjie 64 5月  10 22:16 6 -> /dev/pts/3
lrwx------ 1 shengjie shengjie 64 5月  10 22:16 7 -> /dev/pts/3
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 9 -> /usr/share/dotnet/shared/Microsoft.NETCore.App/3.0.0/System.Private.CoreLib.dll
lr-x------ 1 shengjie shengjie 64 5月  10 22:16 99 -> /dev/urandom
shengjie@ubuntu:/proc/2449$ cat /proc/net/tcp | grep 13890: 0100007F:1389 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 31529 1 0000000000000000 100 0 0 10 0                     8: 0100007F:1389 0100007F:DBE8 01 00000000:00000000 00:00000000 00000000  1000        0 35017 1 0000000000000000 20 4 29 10 -1                    12: 0100007F:DBE8 0100007F:1389 01 00000000:00000000 00:00000000 00000000  1000        0 28496 1 0000000000000000 20 4 30 10 -1  

过滤 strace2 目录日志,抓取监听在 localhost:5001socket对应的文件描述符。

shengjie@ubuntu:~/coding/dotnet/Io.Demo$ grep 'bind' strace2/ -rn
strace2/io.2449:2243:bind(11, {sa_family=AF_UNIX, sun_path="/tmp/dotnet-diagnostic-2449-23147-socket"}, 110) = 0
strace2/io.2449:2950:bind(29, {sa_family=AF_INET, sin_port=htons(5001), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
strace2/io.2365:4568:bind(10, {sa_family=AF_UNIX, sun_path="/tmp/dotnet-diagnostic-2365-19043-socket"}, 110) = 0
strace2/io.2420:4634:bind(11, {sa_family=AF_UNIX, sun_path="/tmp/dotnet-diagnostic-2420-22262-socket"}, 110) = 0
strace2/io.2402:4569:bind(10, {sa_family=AF_UNIX, sun_path="/tmp/dotnet-diagnostic-2402-22042-socket"}, 110) = 0

从中可以看出同样是29号文件描述符,相关系统调用记录中 io.2449文件中,打开文件,可以发现相关系统调用如下:

shengjie@ubuntu:~/coding/dotnet/Io.Demo$ cat strace2/io.2449 # 截取相关系统调用
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_TCP) = 29
setsockopt(29, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(29, {sa_family=AF_INET, sin_port=htons(5001), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
listen(29, 10) 
accept4(29, 0x7fa16c01b9e8, [16], SOCK_CLOEXEC) = -1 EAGAIN (Resource temporarily unavailable)
epoll_create1(EPOLL_CLOEXEC)            = 32
epoll_ctl(32, EPOLL_CTL_ADD, 29, {EPOLLIN|EPOLLOUT|EPOLLET, {u32=0, u64=0}}) = 0
accept4(29, 0x7fa16c01cd60, [16], SOCK_CLOEXEC) = -1 EAGAIN (Resource temporarily unavailable)

从中我们可以发现 accept4直接返回-1而不阻塞,监听在 127.0.0.1:5001的socket对应的29号文件描述符最终作为 epoll_ctl的参数关联到 epoll_create1创建的32号文件描述符上。最终32号文件描述符会被 epoll_wait阻塞,以等待连接请求。我们可以抓取 epoll相关的系统调用来验证:

shengjie@ubuntu:~/coding/dotnet/Io.Demo$ grep 'epoll' strace2/ -rn
strace2/io.2459:364:epoll_ctl(32, EPOLL_CTL_ADD, 35, {EPOLLIN|EPOLLOUT|EPOLLET, {u32=1, u64=1}}) = 0
strace2/io.2462:21:epoll_wait(32, [{EPOLLIN, {u32=0, u64=0}}], 1024, -1) = 1
strace2/io.2462:42:epoll_wait(32, [{EPOLLOUT, {u32=1, u64=1}}], 1024, -1) = 1
strace2/io.2462:43:epoll_wait(32, [{EPOLLIN|EPOLLOUT, {u32=1, u64=1}}], 1024, -1) = 1
strace2/io.2462:53:epoll_wait(32, 
strace2/io.2449:3033:epoll_create1(EPOLL_CLOEXEC)            = 32
strace2/io.2449:3035:epoll_ctl(32, EPOLL_CTL_ADD, 33, {EPOLLIN|EPOLLET, {u32=4294967295, u64=18446744073709551615}}) = 0
strace2/io.2449:3061:epoll_ctl(32, EPOLL_CTL_ADD, 29, {EPOLLIN|EPOLLOUT|EPOLLET, {u32=0, u64=0}}) = 0

因此我们可以断定同步非阻塞I/O的示例使用的时IO多路复用的epoll模型。

关于epoll相关命令,man命令可以查看下 epoll_create1epoll_ctl和、 epoll_wait系统调用的相关说明:

shengjie@ubuntu:~/coding/dotnet/Io.Demo/strace$ man epoll_create
DESCRIPTIONepoll_create() creates a new epoll(7) instance.  Since Linux 2.6.8, the size argument is ignored, but must begreater than zero; see NOTES below.epoll_create() returns a file descriptor referring to the new epoll instance.  This file descriptor  is  usedfor  all  the subsequent calls to the epoll interface.
shengjie@ubuntu:~/coding/dotnet/Io.Demo/strace$ man epoll_ctl
DESCRIPTIONThis  system  call  performs  control  operations on the epoll(7) instance referred to by the file descriptorepfd.  It requests that the operation op be performed for the target file descriptor, fd.Valid values for the op argument are:EPOLL_CTL_ADDRegister the target file descriptor fd on the epoll instance referred to by the file  descriptor  epfdand associate the event event with the internal file linked to fd.EPOLL_CTL_MODChange the event event associated with the target file descriptor fd.EPOLL_CTL_DELRemove  (deregister)  the  target file descriptor fd from the epoll instance referred to by epfd.  Theevent is ignored and can be NULL (but see BUGS below).
shengjie@ubuntu:~/coding/dotnet/Io.Demo/strace$ man epoll_wait
DESCRIPTIONThe  epoll_wait()  system  call  waits for events on the epoll(7) instance referred to by the file descriptorepfd.  The memory area pointed to by events will contain the events that will be available  for  the  caller.Up to maxevents are returned by epoll_wait().  The maxevents argument must be greater than zero.The  timeout  argument  specifies  the number of milliseconds that epoll_wait() will block.  Time is measuredagainst the CLOCK_MONOTONIC clock.  The call will block until either:*  a file descriptor delivers an event;*  the call is interrupted by a signal handler; or*  the timeout expires.

简而言之,epoll通过创建一个新的文件描述符来替换旧的文件描述符来完成阻塞工作,当有事件或超时时通知原有文件描述符进行处理,以实现非阻塞的线程模型。

总结

写完这篇文章,对I/O模型的理解有所加深,但由于对Linux系统的了解不深,所以难免有纰漏之处,大家多多指教。 同时也不仅感叹Linux的强大之处,一切皆文件的设计思想,让一切都有迹可循。现在.NET 已经完全实现跨平台了,那么Linux操作系统大家就有必要熟悉起来了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/309965.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[设计模式]外观模式

外观模式:为一组具有类似功能的类群&#xff0c;比如类库&#xff0c;子系统等等&#xff0c;提供一个一致的简单的界面。 代码如下: #include <iostream> using namespace std;class Television { public:void on(){cout << "Tv on" << endl;}v…

Keywords Search HDU - 2222(AC自动机模板)

题意&#xff1a; 给定 n个长度不超过 50的由小写英文字母组成的单词准备查询&#xff0c;以及一篇文章&#xff0c;问&#xff1a;文中出现了多少个待查询的单词。多组数据。 题目&#xff1a; In the modern time, Search engine came into the life of everybody like Go…

介绍一个基于 .NET 的船的新 PHP SDK + Runtime: PeachPie

前言这几天想基于 .NET Core 搞一个自己的博客网站&#xff0c;于是在网上搜刮各种博客引擎&#xff0c;找到了这些候选&#xff1a;Blogifier、Miniblog 以及 edi 写的 Moonglade。Blogifier&#xff1a;这是前端是个 Angular SPA 应用&#xff0c;不利于 SEO&#xff0c;同时…

[设计模式]适配器模式

适配器模式:将一个类的接口转换成客户希望的另外一个接口&#xff0c;使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 &#xff08;将已经写好的&#xff0c;但是不符合需求的接口&#xff0c;转换成目标接口&#xff09; 代码如下: #include <iostream>…

[工具]微软的学习平台Microsoft Learn很好用,推荐一下

1. 什么是Microsoft LearnMicrosoft Learn是微软这两年大力推广的全新学习平台&#xff0c;可提供 Microsoft 产品交互式学习体验。基本上无需登录即可使用&#xff0c;但登录后可以使用更多功能&#xff0c;包括&#xff1a;累积分数和成就跟踪学习活动进度使用免费的 Azure 资…

多终端数据同步机制设计

多终端数据同步机制设计之前写过一篇文章数据同步流程设计的文章&#xff0c;这里整理一下在公众号里分享一下Intro因为项目需要&#xff0c;需要设计一个多终端数据同步的机制&#xff0c; 需要满足以下条件&#xff1a;多个终端数据操作及同步&#xff0c;终端可能离线每次同…

Popular Cows POJ - 2186(tarjan算法)+详解

题意&#xff1a; 每一头牛的愿望就是变成一头最受欢迎的牛。现在有 N头牛&#xff0c;给你M对整数&#xff08;A,B&#xff09;&#xff0c;表示牛 A认为牛B受欢迎。这种关系是具有传递性的&#xff0c;如果 A认为 B受欢迎&#xff0c; B认为 C受欢迎&#xff0c;那么牛 A也认…

[设计模式]装饰模式

装饰模式: 通过AbstractEquipment装饰AbstractHero&#xff0c;使其heroA增加了一个穿装备的功能。 代码如下: #include <iostream> using namespace std;class AbstractHero {public:virtual void showStatus() 0;int hp;int mp;int at;int df; };class HeroA :publi…

ASP.NET Core分布式项目实战(Consent Controller Get请求逻辑实现)--学习笔记

任务20&#xff1a;Consent Controller Get请求逻辑实现接着上一节的思路&#xff0c;实现一下 ConsentController根据流程图在构造函数注入 IClientStore&#xff0c;IResourceStore&#xff0c;IIdentityServerInteractionService构造函数private readonly IClientStore _cli…

[设计模式]观察者模式

代码如下: #include <iostream> #include <list> using namespace std;class AbstractHero { public:virtual void update() 0; };class HeroA :public AbstractHero { public:HeroA(){cout << "英雄A正在打BOSS" << endl;}virtual void u…

RMQ算法讲解

版权声明&#xff1a;本文为博主原创文章&#xff0c;遵循 CC 4.0 BY-SA 版权协议&#xff0c;转载请附上原文出处链接和本声明。 本文链接&#xff1a;https://blog.csdn.net/qq_41311604/article/details/79900893 </div><!--一个博主专栏付费入口--><!--一个…

Kubernetes是容器化微服务的圣杯么?

导语Kubernetes已成为山丘之王。开源技术Kubernetes以及随后的发行版正以超快的速度让人们爱上容器技术&#xff0c;并且开始夺回对容器化环境的控制权。不幸的是&#xff0c;编排容器只是战斗进行了一半。正文云服务提供商接连宣布他们的编排选择是Kubernetes私有发行版&#…

[设计模式]命令模式

代码如下: #include <iostream> #include <queue> #include <Windows.h> using namespace std;class HandleClientProtocol { public:void addMoney(){cout << "给玩家增加金币" << endl;}void addDiamond(){cout << "给玩…

Zjnu Stadium HDU - 304 加权并查集

题意&#xff1a; 观众席围成一圈。列的总数是300&#xff0c;编号为1–300&#xff0c;顺时针计数&#xff0c;我们假设行的数量是无限的。将有N个人去那里。他对这些座位提出了要求&#xff1a;这意味着编号A的顺时针X距离坐着编号B。例如&#xff1a;A在第4列&#xff0c;X…

还不明白可空类型原理? 我可要挖到底了

一&#xff1a;背景1. 讲故事做好自媒体到现在有一个月了&#xff0c;关注我的兄弟应该知道我产出了不少文章&#xff0c;号里的粉丝也多起来了&#xff0c;我也尽最大努力做到有问必回&#xff0c;现在是基础的、高深的问题都接踵而来&#xff0c;可我也只是一只小菜鸟&#x…

[设计模式]策略模式

策略模式:定义了一系列算法&#xff0c;并将每一个算法封装起来&#xff0c;而且使它们还可以相互替换。 策略模式让算法独立于使用它的客户而独立变化。 代码如下: #include <iostream> using namespace std;class WeaponStrategy { public:virtual void useWeapon()…

[设计模式]模板方法模式

模板方法模式: 定义一个操作中算法的框架&#xff0c;而将一些步骤延迟到子类中。模仿方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 代码如下: #include <iostream> using namespace std;class DrinkTemplate { public:virtual void Boi…

差距(分享)

非985大学生, 你和别人的差距在哪里?&#xff08;转&#xff09; 非985大学生, 你和别人的差距在哪里? 中国青年报03-24 在知乎上看到这样一段话&#xff1a; “渣学校意味着渣教学&#xff0c;渣教学意味着渣学历&#xff0c;渣学历意味着渣就业&#xff0c;就算以后考了研究…

在微服务框架Demo.MicroServer中添加SkyWalking+SkyApm-dotnet分布式链路追踪系统

1.APM工具的选取Apm监测工具很多&#xff0c;这里选用网上比较火的一款Skywalking。Skywalking是一个应用性能监控(APM)系统&#xff0c;Skywalking分为服务端Oap、管理界面UI、以及嵌入到程序中的探针Agent部分&#xff0c;大概工作流程就是在程序中添加探针采集各种数据发送给…

计算机组成原理期末复习题

地址总线A15~Ao(低),存储空间(按字节编址)分配如下 2000H~3FFFH为ROM区, 5000H~6FFFH为RAM区。用 ROM芯片(4Kx4)和RAM芯片(4Kx4)组成该存储器。请回答 &#xff1a;(1)分别需要ROM和RAM多少片? (2)用二进制形式写出每组芯片的地址范围,并说明可以通过哪些地址位来形成片选信号…