UNP 学习笔记 4:网络API的边界问题

UNP 学习笔记 4:网络API的边界问题,第1张

UNP 学习笔记 4:网络API的边界问题

端口号

  • well known 是 1024 以前(UNIX 保留端口,权限限制)
  • 临时端口用 45152 到 65535
  • 1024到5000 以前是 BSD 用来当临时端口的,现在可能不用避免,但是他们也会被 IANA 用来登记一些应用,不是必须避免使用的。
  • 临时端口可以进行尝试,或者直接让 *** 作系统分配进行了。

fork

  • 首先 child 应该关闭 listen fd,防止一些边界情况
  • 然后 parent 应该关闭 connfd 这样 child exit 之后处理文件就把他关闭了。如果父亲不关闭 connfd,就会不断消耗端口。(close 的好处就是不用 wait child,而托管内核 RAII shared_ptr 了属于是)。

read

  • 先复习 CSAPP 的内容(超,我全都忘光了,但是发现 csapp 不过是 apue 的二道贩子):
  • 慢系统调用, 由于慢系统调用可能会永远阻塞程序, 所以Unix规定慢系统调用再接受到中断后会直接错误返回, 并设置 errno 位未 EINTR, 调用者应该自己处理是重启调用还是算了. 我个人认为这个时候他可以成功接收了部分数据,因为有 kernel stack 和 trapframe 在那里。
  • 然后就是文件的阻塞和非阻塞属性, 如网络可能是非阻塞的, 如果没有的希望 read 直接返回, 如果还要再来可以循环调用. 阻塞的话就可能永远阻塞, 才要提出中断推出的设定.
  • 只要用系统调用, 就会出现 short count 的问题, 比如遇到了 EOF, 或者从终端读必须一次读一行, 多了不不了, 少了还回去. 或者缓冲和网络延迟会导致short count, 所以应该用循环来封装 write 和 read, 还有一个问题就是 size_t 的大小要求, 由于 read 必须返回 -1 to indicate a error,  导致不用无符号也减少了能一次传输的大小. CSAPP 编写了无缓存和带缓存的 RIO robust i/o 包提供改进效果和循环调用 write/read.
  • 所以编写好了 readline 和 readn,之后直接使用这些函数。
  • read 返回 0 的情况可以是 FIN 产生了,因为这会引发socket fd 上出现一个 EOF,注意EOF是一个不存在的东西,但是系统调用会引发这个东西,比如 Ctrl+D 的时候 kernel 负责  stdio 的系统调用会解释一个EOF出来,socket 关闭和指针到达文件结尾也是同样道理。

中断的系统调用

  • UNIX 把系统调用分为两种,其中低速系统调用是可能永远阻塞的,除非显式地中断他,不然他不会超时。允许中断也是很重要的,这也是 timer interrupt 实现抢占任务调度的需要(请回顾 syscall 要想不被 timer interrupt 一般需要显式关闭开启 preemt,如 kernel 里的 mutex 实现)。其中 read 也不全是会被中断的,只有慢速设备才会被中断,如磁盘就不会被中断,一般来说,读字符终端、网络的socket描述字,管道文件等,这些文件的缺省read都是阻塞的方式。如果是读磁盘上的文件,一般不会是阻塞方式的。但使用锁和fcntl设置取消文件O_NOBLOCK状态,也会产生阻塞的read效果。
  • 直接规定被中断就直接返回错误而不是继续阻塞应用(进程可能已经捕获到了信号,不如直接把控制权交给应用),出于这样的想法(当捕获到某个信号且相应信号处理函数返回时才 EINTR,默认)。
  • 早期对于中断时已经读写的部分内容,既可以允许 *** 作系统正常返回已经读写的数量给应用,也可以返回错误,如今直接规定错误。
  • POSIX 标准允许自动重启慢速被中断的系统调用,前提是注册了 restart (使用信号注册或者 fcntl)。支持内核上重启可以统一应用程序进行系统调用的行为而不是不同的行为对于不同的调用环境,比如应用程序可能不知道一个文件是快速的还是慢速的 thanks to everything is file 的抽象。
  • accept 本身也是慢系统调用,that‘s why 我们要 wrap 一个 for 循环给他。

(补充内容,应该是网络无关)可重入函数

  • 由于我们需要响应信号,但是有些系统调用是已经存了一套上下文的,而且重要的是一个应用程序只有一套 kernel context  和 user context,所以必须明确知道 handler 是没有 context 的而且要避免调用任何的具备上下文的系统调用,which 是不可重入的。
  • malloc 是不可重入的,因为他可能在维护一个链表状态的更新。
  • 保证 handler 中只使用 async-signal safe 的函数,包括各种调用了不 safe 的 syscall 的库函数也无法使用。我不知道C++ new 和 delete 能不能用 since 他们用了 malloc(百度和 google 都找不到?后来搜到了),理论上分析应该是不能用的,这样会造成死锁。MSC54-CPP. A signal handler must be a plain old function,cppref 则说:

Signal handlers are expected to have C linkage and, in general, only use the features from the common subset of C and C++. It is implementation-defined if a function with C++ linkage can be used as a signal handler.

(until C++17)

There is no restriction on the linkage of signal handlers.

(since C++17)

From

难道 C++ 17 支持使用 c++ 特性在 signal handler 里面应用了?我还是不明白,请使用 clang-tidy 检查 C++ 代码,问题被检测出来。

连接基本情况

  • 对客户端而言,只要他收到 SYN+ACK,connect 就会返回,而这个 ACK 丢失的情况不归应用层的事,而是滑动窗口负责重发的。
  • 对于服务器,必须收到 last ack 建立了三次握手 accept 才能够返回到上层。当他 fork 出来导致程序运行了之后父进程将在循环回到 accept 的时候阻塞。
  • 僵尸进程是为了留下死亡信息可能有人需要。由于没有办法在自己活着的情况下,让孩子变成孤儿,所以我们应该处理僵尸子进程防止浪费资源。为了不阻碍程序控制流,我们使用信号处理程序处理 SIGCHLD 即可。曾经是 Unix98 说如果设置 SIGCHLD 位 SIG_IGN 的话说明没必要保留额外死亡信息(即不用僵尸进程),但是 POSIX 标准没有,所以只能手动处理了。这里就涉及了 fork 服务器必须处理上述可重入函数死锁的问题,如不能使用 printf(线程安全但是一个进程自己无法重入则会死锁)。
  • 然而我们必须在 handler 里面多次调用 wait,这是因为中断是不可靠累计的!所以这里要用 waitpid,我们知道 wait 是阻塞的,如果中断是可靠的阻塞我们的 handler 不会有任何效果,但是如果我们要实现多次调用 wait 就麻烦了,由于无法知道有多少僵尸,所以要使用一个非阻塞的 wait 函数并且循环直到非阻塞返回。

连接异常 I —— 服务端进程终止后

  • 以下是针对一个 echo 服务器的讨论。(理论我做笔记希望复习不需要上下文就能看,不过具体的上下文是 unpv13e 的 第五章,对应源码的 tcpcliserv 文件夹)。具体函数调用是客户端循环:stdin,writen,readn。服务端 fork 后循环 readn,writen。
  • 如果 accept 在返回之前,连接中断了,此时 accept 不应该返回!但是他已经在半路上了,Berkeley 直接透明了这个情况,更一般的情况 POSIX 规定返回 errno 为 EConNABORTED 以便于上层重启 accept。
  • 服务器关闭连接的话,FIN 会来到客户端,客户端可能正在做别的事情而不是在 read ,这样下一次他调用 write 的时候,连接本身(指客户端)应该正在 close wait 状态(TCP 标准)!
  • 而服务器那边进程都结束了属于是(理论上是 FIN_WAIT_2, 摘自 TCP/IP 学习笔记:FIN_WAIT_2 无限等待问题:这个state没有 timeout,如果主动关闭方不允许半关闭,而对方又没有即时回复 FIN,比如服务器请求关闭连接,客户端由于在处理其他事情,或者客户端干脆终止进程没有释放资源(即没有底层网络栈发 FIN),导致无限等待。方法是做一个 timeout )
  • 客户端这下套接字是由内核维护的(TCP标准,必须完成4次挥手吧),但是没有任何上层进程在接收内容(于是他不会直到而且不会调用 close 完成 second fin),客户他继续 write (他不知道已经 close wait 了)的话,会收到一个 RST,这个地方存疑。
  • 这里有一个问题,因为服务端本身理论上是处于 FIN_WAIT_2 的,而他也的确是这样(默认的 close 行为是完成4次挥手但是 time wait 的所有内容都丢弃),他也可以通过 shutdown 或其他选项控制方法直接禁止 fin_wait 状态,流程是不发 FIN 而发 RST 快速中断。
  • 那么这里为什么服务器会发送 RST 呢?这又涉及到内核管理 socket 的方式了,我们直到不 listen 的socket是肯定会 RST 的,不 accept 的则不然,因为三次握手是 kernel 里完成的。那么如果 socket 的 ref cnt = 0的时候内核会怎么办呢?
  • 唯一的方案是采用那个避免 FIN_WAIT_2 无限浪费的方案,设定一个计时器,或者直接判断 refcnt = 0,对端如果还在发数据,就采用 RST  快速关闭方法。
  • RST 的出现会引发一个 read 0,即 EOF,此时应用层应该调用 close 了。如果他继续写的话(读写分离属于是),就会触发 SIGPIPE 信号 Broken pipe: write to pipe with no  readers; see pipe(7),这是客户端 kernel 的处理方式,因为他知道连接被 RST 了,他应该有这个状态信息。
  • 问题是怎么知道出现了 RST 而不是简单的 read 0 信号,解决方案是避免阻塞在其他系统调用上(本例是阻塞在 fget,stdio 上,所以无法马上得到 read 的结果而继续 write 触发 RST)。但是没有 stdin,没有 write 到服务器也不可能等 read 的结果,所以我们还是通过委托内核来实现多阻塞, which is select 和 poll 做的事情。

连接异常 II —— 服务端重启或关机

  • 这里只要知道重启所有老主顾都会收到  RST
  • 关机后的情况和连接异常 I 的分析基本相类似,一个完备的关机应当告知应用程序,所有的后台应用也的确会收到一个信号,然后系统应当延迟一会儿关机。(这就是为什么关机要等一会而不是马上关机,因为要善后)。
  • 其实这部分和 TCP、IP 笔记里面讲的基本符合,应该是参考过的了。不赘述了。

应用层分包

  • 这部分只需要知道要做就行了。
  • 因为不标准序列化的时候直接用 read write 读写结构体也是一种序列化不是吗XD

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/3971075.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-10-21
下一篇 2022-10-21

发表评论

登录后才能评论

评论列表(0条)

保存