UNP 学习笔记 5:高级 IO 与 IO 复用

可惜 UNP 和 APUE 不讲 EPOLL,UNP 的重点内容可能快看完了,之后 UNPv1 就做参考书了,然后之后的 Linux 服务器编程就全是搞 APUE 和 Linux 编程的书了,看来之后还得持续。

以下基本是 APUE 十四章内容

非阻塞IO 使用选项打开文件

  • 通过 O_NONBLOCK 选项作为 flag 调用 open 函数
  • 通过 fcntl 设置同样的选项(本质是一个 flag、mask)
  • 如果不能完成,将会返回错误。(错误的语义是正确的,因为我们希望不block,不能满足我们的期望)
  • 例子是一次 stderr 写大量文件,我们知道stderr 是没有缓冲的,这个缓冲的意思并不是我们屏幕显示的缓冲,而是发送的缓冲,实际终端显示是采用一个缓冲区的(也决定了能容纳的行数),一次 write 大量内容到终端,终端的缓冲区是有限的,所以就必须等到 GUI 操作系统把终端内容发给显示缓存后清空缓冲区,EAGAIN 11 Resource temporarily unavailable。

记录锁

  • 数据库需要保证单独控制文件,所以需要对文件加锁,否则其事务不靠谱(他写的时候以为都写了,结果另一个线程可能在写,所以必须是 OS 层提供锁才行),然而UNIX 早期是没有这种功能的,开放给程序员来控制,这就是为什么一开始学文件IO的时候会感觉不自然。
  • 字节范围锁,由于多线程写的范围可能并不重合,而且一个大文件会有很多部分,就比如数据库可能会用一个大文件存储多个 page,读写的时候单独读某部分 offset(open 之后 fseek 就行了)。
  • manual page 一大段,摘录不了,上连接了fcntl(2) - Linux manual page (man7.org)
  • 可以给 fcntl 传一个锁 flock 结构的指针,指定要锁哪里,这样就能让别人读不了了。不过这个可能是给数据库用的,然而 15-445 都不用操作这么底层的编程属于是,估计之后都是 manipulate buffer pool 而已吧。
           struct flock {
               ...
               short l_type;    /* Type of lock: F_RDLCK,  F_WRLCK, F_UNLCK */
               short l_whence;  /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
               off_t l_start;   /* Starting offset for lock */
               off_t l_len;     /* Number of bytes to lock */
               pid_t l_pid;     /* PID of process blocking our lock
                                   (set by F_GETLK and F_OFD_GETLK) */
               ...
           };
  • 这部分内容实际是很重要的,我这里继续看网络编程了,之后会回来的。

多路 IO 基础

  • 标准的 IO 做法比如先读后写,先写后读都是正常的可以使用阻塞的没问题。
  • 如果需要读两个描述符就输了(比如两个socket缓冲区),因为阻塞在其中一个可能浪费了不如读另一个。
  • 还有一个情况是 socket 的 write 无法知道连接是不是出事了 while read 可以区分 EOF 和阻塞(或者 EAGAIN。还有就是我们前一篇笔记的情况,client 阻塞在标准 io 上从而没有监听到 read 可能返回 0 的情况。
  • 以 telnet 为例,telnet 需要同时读终端输入和 telnetd(deamon)的回显或者提示信息(比如每次命令执行,甚至像用 zsh 的时候输入一个字母就要返回提示),和我们上一篇说的那个问题一样,但是阻塞式无法在这里用。
  • 这篇第一节提到 non blocking,所以一种方法是使用非阻塞轮询,然而这等于 spinlock 了属于是,直接浪费资源 while 多用户系统每个人都要轮转,结果这里可能时间都用来轮询了 while 我们本来可以让给其他用户利用 CPU 资源。
  • 第二种方法是用多进程(fork)或者多线程分别阻塞,但是这样又涉及 IPC 和各种 signal handling 的操作,还有contention 。这下得不偿失,还不如轮询了属于是(虽然,其实阻塞还是节约的,就是编程难一点吧?)。
  • IO multiplexing 是 kernel 上管理多个 wchan 的,我实在是不知道 chan 是什么,之前做 xv6 学内核编程的时候 sleeplock  也是睡在 chan 上,为什么不叫 condition 呢?不过 w 是 wait 的意思应该。

select::fd_set 描述符集合

  • 有一种方法来传一堆 fd,比如 bitmap,但是实际我们 leave 给 implementation 就行了,没必要指定怎么实现。回想malloc lab 的那一堆 macro 的思路,我们用抽象 adt 来表示就行了。

FD_ZERO()
              This macro clears (removes all file descriptors from)
set.
              It should be employed as the first step in initializing a
              file descriptor set.

FD_SET()
              This macro adds the file descriptor
fd to set.  Adding a
              file descriptor that is already present in the set is a
              no-op, and does not produce an error.

FD_CLR()
              This macro removes the file descriptor
fd from set.
              Removing a file descriptor that is not present in the set
              is a no-op, and does not produce an error.

FD_ISSET()
             
select() modifies the contents of the sets according to
              the rules described below.  After calling
select(), the
             
FD_ISSET() macro can be used to test if a file descriptor
              is still present in a set. 
FD_ISSET() returns nonzero if
              the file descriptor
fd is present in set, and zero if it
              is not.

From <select(2) - Linux manual page>

  • 使用的时候第一个参数是 int fd。然后注意首先要 zero 然后再开始设(结构体会不会默认全0初始化我忘记了。。),总之记得以前编程未初始化的痛苦就行了,一定要记得 explicit 初始化。

select::何时 ready

  • readfds 如果能不阻塞 read 就返回。write 同理
  • exceptfds 是具备未决一场条件时,如带外数据(rfc声称new application请不要使用带外数据),或者发生了条件。
  • 注意,EOF 会被认为是 read ready(前一篇说的 RST 也算这部分内容)。
  • 这里 APUE 的内容不够详细针对socket,我有必要回去看 UNP。(APUE后面还有poll和异步io的内容)

然后补充 UNP 的 Select 部分的内容。UNP 第六章(为什么 steven 一个内容分两本书写,侧重点和详细点还都不一样,属实奇才)。

主要记录 select 的要点。

       int select(int nfds, fd_set *readfds, fd_set *writefds,

                  fd_set *exceptfds, struct timeval *timeout);

       void FD_CLR(int fd, fd_set *set);

       int  FD_ISSET(int fd, fd_set *set);

       void FD_SET(int fd, fd_set *set);

       void FD_ZERO(fd_set *set);

需要 IO 复用的场景

  • telnet,交互输入与套接字,和第五章的问题一样 fget + socket read。
  • 客户并行工作两个套接字。
  • 服务器 listen 和连接(注意这种即非fork版连接IO复用网络编程模型)
  • TCP + UDP(什么带外传输替代方案)。
  • inetd 的实现需要处理多协议多服务(这个厉害,和 systemd 那些),学了才明白后台任务那些是什么意思,之前都是用不知其所以然,之后得具体看看deamon相关的东西。

IO模型速览

  • 阻塞 IO 这个很熟悉了,单一 fd 能力或者有问题。
  • 非阻塞 IO 轮询,空转
  • 多线程阻塞 IO,线程调度与 IPC 问题
  • IO multiplexingselect poll通过阻塞在 select 或者poll multiplexer,然后扩展一堆外设接口了(什么数电)
  • signal driven 通过设定套接字,然后用 SIGIO 来实现事件驱动。
  • 不过最后还是io复用赢了,因为 epoll 赢麻了。(epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。)epoll 的学习我感觉最早也要过一两天吧。
  • 异步 IO,这个刚刚在 APUE 没继续看。主要就是委托给kernel 去做,而不阻塞应用层。和 IO multiplexing 反过来了。(后者事件为可以启动,前者事件为已经完成)。然而 socket 一般可能没有 aio 的实现(需要内核支持)。

Select 就绪条件 概述

  • 首先基本用不到 exceptfd 尽管思想实验上好像很有用,但是异常的语义很少定义,基本和网络编程无缘 -分了。
  • csdn 上传图片这里出问题了

就绪条件可读

  • 我们可以做思想实验怎么让 kernel 知道可以达到不阻塞调用呢?答案是看接收缓冲区,即窗口。我们可以规定一个 low water mask,which is default set to 1,就是说有一个 byte 数据就能返回了,read 不会阻塞,赢!
  • 读部分被关闭的时候,shutdown 了,接收到 FIN 了的意思。这种情况返回0(EOF),也算不阻塞。
  • listen socket accept 可以返回的时候。
  • 套接字错误。

就绪条件可写

  • 刚刚建立连接(握手)。
  • 发送缓冲区可用(我们说过了 write 返回不意味已经发送,而是进入kernel)。low water 是2048.
  • UDP (不用连接直接可以,但是还是有缓冲区 low water,由于 UDP 不用重发,基本送给网卡之后就再次就绪了)。
  • SIGPIPE 写部分关闭。
  • connect 成功或者失败
  • 错误

信号/中断复习

  • select 当错误时会返回 -1 并且设置 errno。
  • 复习操作系统的课程,我们必须认识到 linux信号处理会唤醒sleep的进程,这是为了方便某些情况的无限阻塞。
  • 所以 block 在 select 的 process 收到信号的时候是会醒过来的,而且这里 select 本身在 syscall 里面,所以他会直接返回到 user space()。
  • 这一点我又忘记了,总之复习之前讲 read write 的时候对信号是怎么说的:系统调用提前返回时,检测系统调用函数的返回值,如果返回值为 -1 并且 errno 是 EINTR ,那么就知道了该系统调用被中断打断了,需要重新调用该函数。
  • 复习前面那篇网络 API 与边界里面对慢系统调用的说法:UNIX 直接规定被中断就直接返回错误而不是继续阻塞应用(进程可能已经捕获到了信号,不如直接把控制权交给应用),出于这样的想法。
  • 如果进程在一个慢系统调用(slow system call)中阻塞时,当捕获到某个信号且相应信号处理函数返回时,这个系统调用被中断,调用返回错误,设置errno为EINTR(相应的错误描述为“Interrupted system call”)。

信号/中断 II

  • 前面没有讲到的一个内容是 POSIX 对于 signal 没有缓冲区的一个解决方案。可惜 APUE 今天不在手上,决定直接看csdn缝合版了属于是。
  • 基本上是因为信号处理本身是没有任何等待的,快速的两次信号到达,是有可能函数路径没走完马上触发另一个处理函数的。
  • A signal may be blocked, which means that it will not be delivered until it is later unblocked.   Between  the time when it is generated and when it is delivered a signal is said to be pending.
  • Each thread in a process has an independent signal mask, which indicates the set of signals that the thread is currently blocking. 
  • A thread can manipulate its signal mask using pthread_sigmask(3).  In a traditional  single-threaded application, sigprocmask(2) can be used to manipulate the signal mask.
  • 总之就是对于 blocked 的信号,不会响应(被 deliver),即不会传达到 handler。但是注意,并不意味着不会进入信号队列,一旦 unblock 的时候,累积的可靠信号将被重新发送!
  • 不可靠信号:1~31(这些即正规的 POSIX 信号,同csapp说的那样一般只有一个缓存fifo),32 之后的叫做 sigrtmin+n,sigrtmax-m,rt 的意思是实时信号(他们会进行排队)。

pselect 用法

  • 这个是 posix select 的意思,改进+升级属于是。
  • 首先是 timespec 结构替代 timeval 结构,which 用 ns 取代 ms(什么精准施策)。
  • 然后增加了 sigmask 参数来控制中断。
  • pselect() first replaces the current signal mask by the one pointed to by sigmask, then does the "select" function, and  then  restores the original signal mask.
  • 根据上一黑体段的内容,pselect 的 sigmask 就是阻塞一些信号让他们不被响应。
  • 这样才能理解 UNP 这一段话的意思(什么惜墨如金史蒂文斯):
if (intr_flag)

    handle_intr(); /* handle the signal */

if ( (nready = select( ... )) < 0) {

    if (errno == EINTR) {

        if (intr_flag)

             handle_intr();

    }

    ...

}
  • 应当理解这个东西实际是在一个 while True循环里面的(select 每次返回一个 fd 之后就结束了)。
  • 这样就能理解这段代码到底在说什么,每次循环开始,如果 intr flag 了(which is set by signal handler),就调用一个函数。
  • UNP 原文:问题是,在测试intr_flag和调用select之间如果有信号发生,那么若select永远阻塞, 该信号将丢失。
  • 就是说这样来处理信号不靠谱(我绷不住了,你为什么要这样处理信号,直接让 handler 处理逻辑不就没有问题了?可能是为了调用一些不可重入的 syscall 吧)
  • 用 pselect 的写法是这样的:
sigemptyset(&zeromask);

sigemptyset(&newmask);

sigaddset(&new, SIGINT);



sigprocmask(SIG_BLOCK, &newmask, &oldmask);/*阻塞SIG_BLOCK信号*/

if(intr_flag)

       handle_intr();

if((nread=pselect(..., &zeromask))<0){

    if(errno==EINTR){

        IF(intr_flag)

            handle_intr();

    }

    ...

}
  • 首先注意,第一个 flag 判断的时候,他的 interrupt (全局变量设定)必须是在 sigprocmask 调用以前触发的,因为 mask 设定后就不会引发 handler 设置 flag 了。
  • 此时如果在 pselect 调用之前出现了 sigint,他会 block 在缓存位里(但是sigint也是不可靠的),然后 pselect 马上 unblock 就会引发一个 errno 了属于是。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值