UNIX网络编程卷一 学习笔记 第二十五章 信号驱动式IO

news2024/11/27 8:35:13

信号驱动式IO指进程预先告知内核,当某个描述符上发生某事时,内核使用信号通知相关进程,它在历史上曾被称为异步IO,但信号驱动式IO不是真正的异步IO,真正的异步IO通常定义为进程执行IO系统调用告知内核启动某个IO操作,内核启动IO操作后立即返回到进程,进程在IO操作发生期间继续执行,当操作完成或遇到错误时,内核以进程在IO系统调用中指定的某种方式通知进程。

非阻塞式IO也不是真正的异步IO,对于非阻塞式IO,内核一旦启动IO操作就不像异步IO那样立即返回,而是等IO操作完成或遇到错误再返回。非阻塞式IO中,内核立即返回的唯一条件是IO操作的完成需要把进程投入睡眠,这种情况内核不启动IO操作。

POSIX通过aio_XXX函数提供真正的异步IO,这些函数允许进程指定IO操作完成时是否产生信号,以及产生什么信号。

源自Berkeley的实现使用SIGIO信号支持套接字和终端设备上的信号驱动式IO,SVR 4使用SIGPOLL信号支持流设备上的信号驱动式IO,且SIGPOLL与SIGIO值相同。

针对一个套接字使用信号驱动式IO要求进程执行以下步骤:
1.建立SIGIO信号的信号处理函数。

2.设置该套接字的属主,通常使用fcntl函数的F_SETOWN命令设置。

3.开启该套接字的信号驱动式IO,通常通过fcntl函数的F_SETFL命令打开O_ASYNC标志来完成。

O_ASYNC标志是相对较晚加入到POSIX规范中的,支持该标志的系统不多见,因此我们也可用ioctl函数的FIOASYNC请求开启信号驱动式IO。POSIX选用的名字并不恰当,使用O_SIGIO作为此标志名更好。

我们应在设置套接字属主前建立信号处理函数,在源自Berkeley的实现中,这两个步骤的调用顺序无关紧要,因为SIGIO的默认行为是忽略该信号,即使颠倒调用顺序,在调用fcntl后,调用signal前有较小机会产生SIGIO信号,此时信号只是被丢弃。但在SVR 4中,头文件sys/signal.h把SIGIO定义为SIGPOLL,而SIGPOLL的默认行为是终止进程,因此在SVR 4中,我们必须先安装信号处理函数,再设置套接字属主。

很容易把一个套接字设置成以信号驱动式IO模式工作,但哪些条件导致内核产生递交给套接字属主的SIGIO信号取决于具体协议。

对于UDP,SIGIO信号在发生以下事件时内核通过调用sorwakeup产生:
1.数据报到达套接字。

2.套接字上发生异步错误。

当捕获对于某UDP套接字的SIGIO信号时,我们调用recvfrom读入到达的数据或获取发生的异步错误。UDP发生异步错误的前提是UDP套接字已连接。

不幸的是,信号驱动式IO对TCP套接字近乎无用,因为该信号产生得过于频繁,且它的出现并没有告诉我们发生了什么事件,以下情况均导致对于一个TCP套接字产生SIGIO信号(假设该套接字的信号驱动式IO已开启):
1.监听套接字上某连接请求已完成。

2.某个断联请求已发起。

3.某个断联请求已完成。

4.某个连接已关闭一半。

5.数据到达套接字。

6.数据已从套接字发送走(即收到了已发送数据的确认,输出缓冲区有了空闲空间)。

7.发生某异步错误。

例如,如果一个进程读写同一套接字,那么当有数据到达或以前写的数据得到确认时,SIGIO信号均会产生,且信号处理函数中无法区分这两种情况,如果SIGIO用于这种情形,则TCP套接字应设为非阻塞式,以防read或write函数发生阻塞。我们应考虑只对监听TCP套接字使用SIGIO,因为对于监听套接字,产生SIGIO的唯一条件是某个新连接完成。

作者能找到的信号驱动式IO的唯一现实用途是基于UDP的NTP服务器程序,服务器主循环接收来自客户的一个请求数据报并发送回一个应答数据报,但对于每个客户请求,其工作量不能忽略,对服务器而言,重要的是为每个收到的数据报记录到达时间戳,该值将返回给客户,由客户计算到服务器的RTT。以下是构建这样的UDP服务器的两种方式:
在这里插入图片描述
大多UDP服务器都设计成上图左侧所示方式(包括我们的UDP回射服务器),但NTP服务器采用上图右侧所示的方式,当一个新数据报到达时,SIGIO信号处理函数读入该数据报,同时记录它的到达时刻,然后将它置于进程内的另一队列中,以便服务器主循环移走并处理。尽管这样做使服务器代码变复杂了,但可以精确获取数据报达到的时间戳。

进程可通过设置IP_RECVDSTADDR套接字选项获取所收取UDP数据报的目的地址,有人认为,对于所接收UDP数据报还应返回另两个信息:接收接口(如果主机采用普遍的弱端系统模型,则接收接口和目的地址可能不一致)和数据报到达时刻。

对于IPv6,IPV6_PKTINFO套接字选项返回接收接口,对于IPv4,IP_RECVIF套接字选项也返回接收接口。

FreeBSD还提供SO_TIMESTAMP套接字选项,它在一个timeval结构中以辅助数据的形式返回数据报的接收时刻。Linux则提供SIOCGSTAMP ioctl,它返回一个含有数据报接收时刻的timeval结构。

现给出一个类似上图右侧的例子,使用SIGIO信号接收到达数据报的UDP回射服务器程序,客户程序不用改动,服务器的main函数也不用改动,只需改动dg_echo函数。以下是UDP回射服务器的全局声明:

#include "unp.h"

static int sockfd;

#define QSIZE 8    /* size of input queue */
#define MAXDG 4096    /* max datagram size */

// SIGIO信号处理函数把到达的数据报放入一个队列,该队列是一个DG结构数组,我们把它作为一个环形缓冲区处理
// 每个DG结构包含指向所收取数据报的指针、该数据报长度、指向含有客户协议地址的套接字地址结构的指针、该协议地址的大小
typedef struct {
    void *dg_data;    /* ptr to actual datagram */
    size_t dg_len;    /* length of datagram */
    struct sockaddr *dg_sa;    /* ptr to sockaddr{} w/client's address */
    socklen_t dg_salen;    /* length of sockaddr{} */
} DG;
// 静态分配QSIZE个DG结构
static DG dg[QSIZE];    /* queue of datagrams to process */
static long cntread[QSIZE + 1];    /* diagnostic counter */

static int iget;    /* next one for main loop to process,主循环将处理的下一个数组元素下标 */
static int iput;    /* next one for signal handler to read into,信号处理函数将存放到的下一个数组元素下标 */
static int nqueue;    /* # on queue for main loop to process,队列中供主循环处理的数据报总数 */
static socklen_t clilen;    /* max length of sockaddr{} */

static void sig_io(int);
static void sig_hup(int);

下图是DG数组的一个例子,其中第一个元素指向一个150字节的数据报,与它关联的套接字地址长度为16:
在这里插入图片描述
以下是dg_echo函数,该函数与以上全局声明放在同一文件中:

void dg_echo(int sockfd_arg, SA *pcliaddr, socklen_t clilen_arg) {
    int i;
    const int on = 1;
    sigset_t zeromask, newmask, oldmask;

    // 把套接字保存在一个全局变量中,因为信号处理函数里也要用到它
    sockfd = sockfd_arg;
    clilen = clilen_arg;

    // 初始化已接收数据报队列
    for (i = 0; i < QSIZE; ++i) {    /* init queue of buffers */
        dg[i].dg_data = Malloc(MAXDG);
        dg[i].dg_sa = Malloc(clilen);
        dg[i].dg_salen = clilen;
    }
    iget = iput = nqueue = 0;

    // 为SIGHUP(用于诊断目的)和SIGIO建立信号处理函数
    Signal(SIGHUP, sig_hup);
    Signal(SIGIO, sig_io);
    // 设置套接字属主
    Fcntl(sockfd, F_SETOWN, getpid());
    // 设置信号驱动IO,上面提到过,fcntl的O_ASYNC标志是POSIX设置信号驱动式IO的方式
    // 但由于大多系统还不支持它,我们改用ioctl函数
    Ioctl(sockfd, FIOASYNC, &on);
    // 设置非阻塞式IO,尽管大多系统支持使用fcntl函数O_NONBLOCK标志设置非阻塞式IO
    // 但此处我们仍使用ioctl函数
    Ioctl(sockfd, FIONBIO, &on);

    Sigemptyset(&zeromask);    /* init three signal sets */
    // oldmask用来记录阻塞SIGIO时,原来的信号掩码
    Sigemptyset(&oldmask);
    Sigemptyset(&newmask);
    Sigaddset(&newmask, SIGIO);    /* signal we want to block */

    // 把进程的当前信号掩码保存到oldmask中,然后把newmask逻辑或到当前信号掩码
    Sigprocmask(SIG_BLOCK, &newmask, &oldmask);
    for (; ; ) {
        while (nqueue == 0) {
            // sigsuspend函数先保存当前信号掩码,再把当前掩码设置为其参数,此例中是zeromask
            // 即不阻塞任何信号,sigsuspend函数在进程捕获一个信号且从该信号的处理函数返回后才返回
            // sigsuspend函数总是返回EINTR错误,在返回前它总是会把当前信号掩码恢复为调用时的值
            // 在本例中就是恢复为newmask的值,从而确保sigsuspend函数返回后SIGIO继续被阻塞
            // 如果sigsuspend函数返回后SIGIO信号未被阻塞,我们会进入下一次while循环的条件判断
            // 可能在测试时我们发现nqueue为0,但刚测试完SIGIO信号就被递交了,导致nqueue为1
            // 然后我们才调用sigsuspend进入睡眠,这样就错过了这个信号,除非另有信号发生
            // 否则我们将永远阻塞在sigsuspend函数处
            sigsuspend(&zeromask);    /* wait for datagram to process */
        }

        /* unblock SIGIO */
        Sigprocmask(SIG_SETMASK, &oldmask, NULL);

        Sendto(sockfd, dg[iget].dg_data, dg[iget].dg_len, 0, dg[iget].dg_sa, dg[iget].dg_salen);

        // 修改iget时不用阻塞SIGIO,因为只有主循环使用iget,信号处理函数不改动它
        if (++iget >= QSIZE) {
            iget = 0;
        }

        /* block SIGIO */
        // 修改nqueue前必须阻塞SIGIO,因为主循环和信号处理函数都会改变它
        // 另外我们在循环顶部测试nqueue时也需要SIGIO阻塞着
        // 我们也可以去掉主循环中的两个sigprocmask函数,省得解阻塞SIGIO后又再阻塞它
        // 但这么做会导致整个循环期间SIGIO一直阻塞着,从而降低了信号处理函数的及时性
        // 这么做不会导致数据报的丢失(假设套接字接收缓冲区足够大),但SIGIO信号的递送在阻塞期间一直被拖延
        // 编写执行信号处理的应用时,我们应尽可能减少阻塞信号的时间
        Sigprocmask(SIG_BLOCK, &newmask, &oldmask);
        nqueue--;
    }
}

以下是SIGIO的信号处理函数,它也应和全局声明放到同一文件中:

static void sig_io(int signo) {
    ssize_t len;
    int nread;
    DG *ptr;

    for (nread = 0; ; ) {
        // 如果DG结构数组队列已满,进程就终止,处理这种情况有更合适的方法,如分配额外缓冲区
        // 但就我们的简单例子而言不如直接终止进程
        if (nqueue >= QSIZE) {
            err_quit("receive overflow");
        }

        ptr = &dg[iput];
        ptr->dg_salen = clilen;
        len = recvfrom(sockfd, ptr->dg_data, MAXDG, 0, ptr->dg_sa, &ptr->dg_salen);
        if (len < 0) {
            if (errno == EWOULDBLOCK) {
                break;    /* all done; no more queued to read */
            } else {
                err_sys("recvfrom error");
            }
        }
        ptr->dg_len = len;

        ++nread;
        ++nqueue;
        if (++iput >= QSIZE) {
            iput = 0;
        }
    }
    // cntread是每次SIGIO信号读入的数据报数量直方图
    // SIGHUP信号被递交时,在其信号处理函数中将其显示为诊断信息
    cntread[nread]++;    /* histogram of # datagrams read per signal */
}

编写以上SIGIO信号处理函数遇到的问题是POSIX信号通常不排队,如果我们正在执行信号处理函数,期间SIGIO信号会被阻塞,如果期间SIGIO信号又发生了2次,则期间发生的这两次SIGIO信号之后只会再递送一次。

POSIX提供一些排队的实时信号,但SIGIO等信号通常不排队。

考虑以下情形:一个数据报到达导致SIGIO被递交,它的信号处理函数读入该数据报并把它放到供主循环读取的队列中,但在信号处理函数执行期间,又有两个数据报到达,导致SIGIO再产生2次,由于SIGIO被阻塞,当它的信号处理函数返回时,该处理函数仅再被调用一次,该信号处理函数的第二次执行读入第二个数据报,第三个数据报仍会留在套接字接收队列,第三个数据报被读入的条件是第四个数据报到达,当第四个数据报到达时,被读入并放到供主循环读取的队列中的是第三个而非第四个数据报。

既然信号是不排队的,开启信号驱动式IO的描述符通常也被设为非阻塞式,这样我们就可以把SIGIO信号的处理函数编写成在一个循环中执行读入操作,直到该操作返回EWOULDBLOCK。

主循环有另一种有问题的写法:

for (; ; ) {
    Sigprocmask(SIG_BLOCK, &newmask, &oldmask);
    while (nqueue == 0) {
        sigsuspend(&zeromask);    /* wait for datagram to process */
    }
    --nqueue;

    /* unblock SIGIO */
    Sigprocmask(SIG_SETMASK, &oldmask, NULL);
    
    Sendto(sockfd, dg[iget].dg_data, dg[iget].dg_len, 0, dg[iget].dg_sa, dg[iget].dg_salen);
    
    if (++iget >= QSIZE) {
        iget = 0;
    }
}

以上写法的错误在于,nqueue是在回射数组元素dg[iget]前递减的,这可能导致信号处理回射把新的数据报从套接字读出来并存到这个数组元素。如当前DG数组已满,iget和iput指向同一元素,此时信号处理函数中nqueue的值为QSIZE-1,从而不会分配更多空间存放数据报,而是在当前iput处存放收到的套接字,导致iget指向的元素还未使用就被覆盖。

以下是SIGHUP的信号处理函数,它显示cntread数组内容,该函数也应和全局声明放在一个文件中:

static void sig_hup(int signo) {
    int i;

    for (i = 0; i <= QSIZE; ++i) {
        printf("cntread[%d] = %ld\n", i, cntread[i]);
    }
}

为了说明信号是不排队的,且除了设置套接字的信号驱动式IO标志外,还必须把套接字设置为非阻塞式,我们与6个客户一起运行以上回射服务器,每个客户发送3645行让服务器回射的文本(即3645个数据报,每个数据报是一行),且每个客户都从同一个shell脚本以后台方式启动,从而使所有客户几乎在同一时刻启动。所有客户终止后,我们向服务器发送SIGHUP信号,显示cntread数组内容:
在这里插入图片描述
大多情况下信号处理函数每次被调用只读入一个数据报,但有些情况会读入多个数据报。cntread[0]计数器不为0是可能的:SIGIO信号在其信号处理函数执行时产生,且在信号处理函数的本次执行中就预先读入了这些信号对应的数据报,当信号处理函数因这些信号的再次递交而被调用时,已经没有剩余的数据报可读了。最后我们验证该数组元素的加权总和等于6个客户发送的文本行数:15899*1+2099*2+515*3+57+4=6*3645=21870

信号驱动式IO就是让内核在套接字上发生某事时使用SIGIO信号通知进程:
1.对于已连接TCP套接字,有很多情况都会导致该通知,反而使这个特性几近无用。

2.对于监听TCP套接字,这种通知发生在有一个新连接准备好被接受时。

3.对于UDP套接字,这种通知意味着一个数据报或异步错误到达,这两种情况我们都调用recvfrom。

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

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

相关文章

Kafka-partition和消费者的关系

Kafka-partition 目录概述需求&#xff1a; 设计思路实现思路分析1.Kafka-partition2.消费者数量小于分区数量3. 拓展实现 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a…

五笔打字练习经验总结

背景 我之前对键盘不太熟悉&#xff0c;打字的速度我测了一下大概是在30/m的样子&#xff0c;于是想提高自己的打字速度&#xff0c;就练习了下&#xff0c;现在大概到了60/m。由于自己打字拼音打字错误率较高&#xff0c;因为手指键位练习不到位&#xff0c;已经很难纠正了。所…

LeetCode[148]排序链表

难度&#xff1a;Medium 题目&#xff1a; 给你链表的头结点 head &#xff0c;请将其按 升序 排列并返回 排序后的链表 。 示例 1&#xff1a; 输入&#xff1a;head [4,2,1,3] 输出&#xff1a;[1,2,3,4]示例 2&#xff1a; 输入&#xff1a;head [-1,5,3,4,0] 输出&…

IOR的安装及使用

简介 IOR 是一种并行 IO 基准测试&#xff0c;可用于测试使用各种接口和访问模式的并行存储系统的性能。IOR存储库还包括mdtest基准测试&#xff0c;专门测试不同目录结构下存储系统的峰值元数据速率。两个基准测试都使用通用并行 I/O 抽象后端&#xff0c;并依赖 MPI 进行同步…

Vue复选框、下拉框使用案例,复选框选项元素(el-checkbox)换行竖向显示

一、复选框 1、<el-checkbox-group></el-checkbox-group>的选项元素默认是行横向显示 <el-checkbox-groupv-model"additionalPermissionsParams.permissionList"change"permissionChange($event)"><el-checkbox label"10"…

c++学习(红黑树)[20]

概念 红黑树&#xff08;Red-Black Tree&#xff09;是一种自平衡的二叉搜索树&#xff0c;它在插入和删除节点时通过一系列的旋转和重新着色操作来保持树的平衡。红黑树的平衡性质使得它在插入、删除和查找等操作上具有较好的性能。 红黑树具有以下特点&#xff1a; 每个节…

【项目开发】商城 - 三级分类 - 简单笔记

目录标题 后端业务类实体类 前端最终实现效果排序变化批量删除 后端 业务类 // 省略其他简单的CRUDOverridepublic List<CategoryEntity> listWithTree() {// 1、查出所有分类List<CategoryEntity> list baseMapper.selectList(null);// 2. 找出所有的一级分类Li…

NOAA官网下载的气象雷达原始数据转化为NC文件详细步骤

一、准备工作 1.先在NOAA官网下载好气象雷达原始数据 NOAA官网下载气象雷达资料详细步骤_珞瑜的博客-CSDN博客 下载好的雷达数据有两种类型Level-2和Level-3。 如上图所示,为气象雷达数据的Level-2产品,站点名字:K

springboot mybatis-plus 多数据源配置(HikariCP)

1.导入依赖jar <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.postgresql</groupId><artifactId>postgres…

iOS--虚拟内存

参考文章 要想了解什么是VM Regions&#xff0c;就得先了解什么是虚拟内存。当我们向系统申请内存时&#xff0c;系统并不会给你返回物理内存的地址&#xff0c;而是给你一个虚拟内存地址。每个进程都拥有相同大小的虚拟地址空间&#xff0c;对于32位的进程&#xff0c;可以拥有…

【N32L40X】学习笔记06-串口dma空闲中断+dma接收数据

串口dma 8 个可独立配置的 DMA 通道。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VchCudlf-1689952378831)(./picture/dma.png)] 实例代码 串口dma使用的是串口绑定方式实现串口的dma数据传输 bsp_uart_dma.h #ifndef _BSP_UART_DMA_H_ #def…

STM32(HAL库)驱动(1.44寸)TFT-LCD彩屏

目录 1、简介 2、CubeMX初始化配置 2.1 基础配置 2.1.1 SYS配置 2.1.2 RCC配置 2.2 屏幕引脚配置 2.3 项目生成 3、KEIL端程序整合 3.1 LCD驱动添加 3.2 函数修改 3.2.1 lcd.h修改 3.2.2 lcd_innit.h 修改 3.2.3 lcd.c修改 3.2.4 lcd_inut.c修改 3.3 主函数代码 3.3…

网络安全(零基础)自学

一、网络安全基础知识 1.计算机基础知识 了解了计算机的硬件、软件、操作系统和网络结构等基础知识&#xff0c;可以帮助您更好地理解网络安全的概念和技术。 2.网络基础知识 了解了网络的结构、协议、服务和安全问题&#xff0c;可以帮助您更好地解决网络安全的原理和技术…

Spring Cloud Alibaba 集成 Skywalking 链路追踪

Spring Cloud Alibaba 集成 Skywalking 链路追踪 简介 skywalking 是一个国产开源框架&#xff0c;2015 年由吴晟开源 &#xff0c; 2017 年加入 Apache 孵化器。skywalking 是分布式系统的应用程序性能监视工具&#xff0c;专为微服务、云原生架构和基于容器&#xff08;Doc…

前端 | ( 十)HTML5简介及相关新增属性 | 尚硅谷前端html+css零基础教程2023最新

学习来源&#xff1a;尚硅谷前端htmlcss零基础教程&#xff0c;2023最新前端开发html5css3视频 文章目录 &#x1f4da;HTML5简介&#x1f407;什么是HTML5&#x1f407;HTML5 优势&#x1f407;HTML5兼容性 &#x1f4da;新增语义化标签&#x1f407;新增布局标签&#x1f407…

怎样原生制作lis的CentOS容器镜像

本文介绍从一个空白的裸机CentOS自己构造检验允许的docker环境。来达到运行环境的高度定制&#xff0c;而不是只能依赖VS或者微软或者数据库厂商提供的镜像当做基础制作。更容易理解基础原理。最终输出产物为lisnew.tar&#xff0c;一个开箱即用的lis运行环境。 制作的整个过程…

自动驾驶分级和技术架构

标题SAE 和 NHTSA自动驾驶分级 当前全球汽车行业中两个最权威的分级系统由美国国家公路交通安全管理局&#xff08;NHTSA&#xff09;和国际自动化工程师协会(SAE)提出。2013年&#xff0c;NHTSA将驾驶自动化的描述分为5个层级。2014年1月&#xff0c;SAE制定J3016自动驾驶分级…

【深度学习-神经网络架构-通俗易懂的入门课程】

文章目录 深度学习与AI的关系机器学习的流程机器学习的核心以及问题深度学习要解决的问题模型如何搭建&#xff1f;特征如何提取&#xff1f;为什么要深度学习&#xff1f; 深度学习的应用深度学习的问题计算机视觉任务分类与检索如何实现分类 神经网络基础线性函数损失函数防止…

Golang 中的可测试示例函数(Example Function)详解

Golang 可测试示例含函数 (Example Function) 示例函数类似于单元测试函数&#xff0c;但没有 *testing 类型的参数。编写示例函数也是很容易的&#xff1a; 创建对应的测试文件&#xff1a;在 Go 项目的源代码目录下创建一个新的文件&#xff08;和被测代码文件在同一个包&…

Java 知识合集 | 多线程与并发

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的编码爱好者 大家好&#xff0c;我是 DevO…