TinywebServer代码详解-定时器处理非活动连接-上(7)
TinywebServer代码详解– 定时器处理非活动连接-上(7)
该blog内容转自:最新版Web服务器项目详解 - 07 定时器处理非活动连接(上)
部分内容参考自:爱编程的大丙(点击跳转)
该blog对上述内容进行补充(在本人的角度)
结合此前记录的blog一起学习:牛客WebServer项目实战(点击跳转)
原项目地址(点击跳转)
博主添加注释后项目地址(点击跳转)
一、基础知识
1.定时器定时方法
本项目中,服务器主循环为每一个http
连接创建一个定时器,并对每个连接进行定时。另外,利用升序时间链表容器将所有定时器串联起来,若主循环接收到定时通知,则在链表中依次执行定时任务
Linux
下提供的三种定时方法:
- socket选项
SO_RECVTIMEO
和SO_SNDTIMEO
SIGALRM
信号I/O
复用系统调用的超时参数
三种方法没有一劳永逸的应用场景,也没有绝对的优劣。由于项目中使用的是SIGALRM
信号,这里仅对其进行介绍,另外两种方法可以查阅游双的Linux高性能服务器编程 第11章 定时器
具体的,利用alarm
函数周期性地触发SIGALRM
信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源
从上面的简要描述中,可以看出定时器处理非活动连接模块,主要分为两部分,其一为定时方法与信号通知流程,其二为定时器及其容器设计与定时任务的处理
2.相关概念
非活跃
- 是指客户端(这里是浏览器)与服务器端建立连接后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费
定时事件
- 是指固定一段时间之后触发某段代码,由该段代码处理一个事件,如从内核事件表删除事件,并关闭文件描述符,释放连接资源
定时器
- 是指利用结构体或其他形式,将多种定时事件进行封装起来。具体的,这里只涉及一种定时事件,即定期检测非活跃连接,这里将该定时事件与连接资源封装为一个结构体定时器
定时器容器
- 是指使用某种容器类数据结构,将上述多个定时器组合起来,便于对定时事件统一管理。具体的,项目中使用升序链表将所有定时器串联组织起来
3.本文内容
本篇将介绍定时方法与信号通知流程,具体的涉及到基础API
、信号通知流程和代码实现
基础API
,描述sigaction
结构体、sigaction
函数、sigfillset
函数、SIGALRM
信号、SIGTERM
信号、alarm
函数、socketpair
函数、send
函数
信号通知流程,介绍统一事件源和信号处理机制
代码实现,结合代码对信号处理函数的设计与使用进行详解
4.可重入函数
函数的可重入性(reentrancy
)指的是在任何时刻,一个函数可以安全地被多个执行线程并发调用,而不会产生不良的副作用。可重入函数通常不依赖于任何共享的、可变的状态(如全局变量或静态变量),并且不使用对其他可重入函数不安全的资源(如动态内存分配和文件操作)
可重入函数特点:
无全局变量或静态变量: 函数内部不使用或修改全局变量或静态变量,或者在修改前对其进行适当的保护
无共享资源: 不依赖或修改共享的资源,除非对其进行适当的同步保护
不调用不可重入函数: 函数内部不调用其他不可重入的函数
避免可重入函数:
避免使用全局或静态变量: 使用局部变量或传递参数来代替
使用线程本地存储(
Thread-Local Storage
): 在需要全局变量时,使用线程本地存储,使每个线程都有自己独立的变量副本保护共享资源: 使用同步机制(如互斥锁)保护对共享资源的访问
避免调用不可重入函数: 如果必须调用不可重入函数,可以使用同步机制来保护这些调用
5.管道
管道必须凑齐读写双方才可以实现
管道可以看作为队列。先入先出,队首出,队尾入
在C语言中,使用管道(pipes
)进行进程间通信是一种经典方法。管道主要用于进程间传递数据
管道由内核提供,单工,具有自同步机制
自同步机制:管道的自同步机制指的是管道在进程间通信时内置的同步特性,这些特性确保了数据的一致性和顺序性。管道通常是阻塞的,这意味着在特定情况下,读操作和写操作会被阻塞,直到某些条件得到满足。这种行为形成了管道的基本自同步特性。
管道分为:匿名管道与命名管道
(1)匿名管道
匿名管道是一种简单的进程间通信(IPC
)机制,主要用于有父子关系的进程间的通信。在Unix
和类Unix
系统(如Linux
)中,匿名管道非常常见。它们是单向的通信方式,通常用于一个进程向另一个进程(具有父子关系的进程)发送数据
基本特性
单向通信:数据只能在一个方向上流动,要么是从父进程到子进程,要么是从子进程到父进程
临时通信通道:管道在使用完毕后会被销毁
数据流:管道中的数据是按照先进先出(
FIFO
)的顺序流动的内存中的存在:匿名管道不对应于文件系统中的任何文件,它们仅存在于内存中
创建与使用
- 在C语言中,可以通过
pipe()
系统调用来创建一个匿名管道。这个调用会创建管道并返回两个文件描述符:一个用于读(pipefd[0]
),另一个用于写(pipefd[1]
)
(2)pipe()
在Unix和类Unix系统中,pipe()
函数用于创建一个匿名管道,用于在进程间进行通信。这个函数是进程间通信(IPC
)的基础工具之一,特别是在需要在有父子关系的进程之间传递数据时。
函数原型
在 C 语言中,pipe()
函数的原型定义在 <unistd.h>
头文件中,如下所示:
1 |
|
参数
pipefd
:一个包含两个整型元素的数组。函数成功执行后,pipefd[0]
会被设置为管道的读端,而pipefd[1]
会被设置为管道的写端
返回值
- 成功时,返回
0
- 失败时,返回
-1
并设置errno
以指示错误原因
使用方式
当你调用 pipe()
函数时,它会创建一个管道并提供两个文件描述符(0端作为读取端口,1端作为写入端口):一个用于读取管道(pipefd[0]
),另一个用于写入管道(pipefd[1]
)。这些文件描述符可以在后续的读写操作中使用
实例程序:
1 | /* |
二、基础API
1.信号的概念
信号是软件层面的中断,信号的响应依赖于中断
(signal
)是一种用于异步通知进程某个事件已经发生的机制。当进程收到一个信号时,它可以有几种不同的反应,包括忽略该信号、执行默认的信号处理动作,或者调用一个用户定义的处理函数
2.信号集
(1)阻塞/未决信号集
在PCB
(Process Control Block
,进程控制块)中有两个非常重要的信号集。一个称之为阻塞信号集
,另一个称之为未决信号集
。这两个信号集体现在内核中就是两张表。但是操作系统不允许我们直接对这两个信号集进行任何操作,而是需要自定义另外一个集合,借助信号集操作函数来对PCB
中的这两个信号集进行修改
未决信号集(pending signal set
)是一个与进程相关的数据结构,用于跟踪那些已经被发送但尚未被处理的信号。每个进程都有一个未决信号集,用于记录哪些信号已被送达但还没有执行对应的信号处理程序.当一个信号被发送给进程时,如果该信号被屏蔽(即进程当前不处理此信号),则信号会被添加到未决信号集中.如果进程解除对某个信号的屏蔽,并且该信号在未决信号集中,那么该信号将从未决信号集中移除,并调用相应的信号处理程序.当信号处理程序运行时,处理该信号并从未决信号集中移除该信号
阻塞信号集(blocked signal set
)用于管理当前进程不允许处理的信号集合。进程可以通过修改其阻塞信号集来控制哪些信号被屏蔽(阻塞)而不被立即处理。被阻塞的信号不会丢失,而是进入未决信号集,当解除阻塞时这些信号才会被处理
未决信号集与阻塞信号集的关系:
信号发送和阻塞
- 当一个信号发送给进程时,操作系统首先检查该信号是否在进程的阻塞信号集中
- 如果该信号在阻塞信号集中,则该信号不会被立即处理,而是被添加到未决信号集中
信号解除阻塞
- 进程解除对某个信号的阻塞时,如果该信号在未决信号集中,操作系统将从未决信号集中移除该信号,并立即处理该信号(即调用相应的信号处理程序或采取默认动作)
信号优先级
- 在解除阻塞时,如果有多个未决信号,通常信号按照优先级顺序进行处理。不同的操作系统可能有不同的信号优先级定义
- 信号的
未决
是一种状态,指的是从信号的产生到信号被处理前的这一段时间 - 信号的
阻塞
是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断某些敏感的操作
阻塞信号集和未决信号集在内核中的结构是相同的,它们都是一个整形数组(被封装过的), 一共 128 字节 (int [32] == 1024 bit
,4字节一个整形,一个字节8bit
,则32*32 = 1024bit
),1024个标志位,其中前31个标志位,每一个都对应一个Linux中的标准信号,通过标志位的值来标记当前信号在信号集中的状态
在阻塞信号集中,描述这个信号有没有被阻塞:
- 默认情况下没有信号是被阻塞的, 因此信号对应的标志位的值为 0
- 如果某个信号被设置为了阻塞状态, 这个信号对应的标志位 被设置为 1
在未决信号集中, 描述信号是否处于未决状态:
- 如果这个信号被阻塞了, 不能处理, 这个信号对应的标志位被设置为1
- 如果这个信号的阻塞被解除了, 未决信号集中的这个信号马上就被处理了, 这个信号对应的标志位值变为0
- 如果这个信号没有阻塞, 信号产生之后直接被处理, 因此不会在未决信号集中做任何记录
(2)信号集函数
因为用户是不能直接操作内核中的阻塞信号集和未决信号集的,必须要调用系统函数,关于阻塞信号集可以通过系统函数进行读写操作,未决信号集只能对其进行读操作
读/写阻塞信号集的函数:
1 |
|
参数:
how
:SIG_BLOCK
:将参数set
集合中的数据追加到阻塞信号集中SIG_UNBLOCK
:将参数set
集合中的信号在阻塞信号集中解除阻塞SIG_SETMASK
:使用参set
结合中的数据覆盖内核的阻塞信号集数据
oldset
:通过这个参数将设置之前的阻塞信号集数据传出,如果不需要可以指定为NULL
sigprocmask()
函数有一个 sigset_t
类型的参数,对这种类型的数据进行初始化需要调用一些相关的操作函数:
1 |
|
未决信号集不需要程序猿修改, 如果设置了某个信号阻塞, 当这个信号产生之后, 内核会将这个信号的未决状态记录到未决信号集中,当阻塞的信号被解除阻塞, 未决信号集中的信号随之被处理, 内核再次修改未决信号集将该信号的状态修改为递达状态(标志位置0)。因此,写未决信号集的动作都是内核做的,这是一个读未决信号集的操作函数:
1 |
|
使用一张图总结信号集操作函数之间的关系:
3.信号捕捉
Linux
中的每个信号产生之后都会有对应的默认处理行为,如果想要忽略这个信号或者修改某些信号的默认行为就需要在程序中捕捉该信号。程序中进行信号捕捉可以看做是一个注册的动作,提前告诉应用程序信号产生之后做什么样的处理,当进程中对应的信号产生了,这个处理动作也就被调用了
(1)signal()
使用 signal()
函数可以捕捉进程中产生的信号,并且修改捕捉到的函数的行为,这个信号的自定义处理动作是一个回调函数,内核通过 signal()
得到这个回调函数的地址,在信号产生之后该函数会被内核调用
1 |
|
参数:
signum
是要捕获的信号handler
是当指定的信号发生时要调用的函数的指针(函数指针,回调函数),或者是以下特定的值:SIG_IGN
:忽略该信号SIG_DFL
:采取信号的默认动作- 该回调函数由内核调用,内核调用回调函数的时候, 会给它传递一个实参,这个实参的值就是捕捉的那个信号值
函数返回值是之前关联到指定信号的处理函数的地址,或者是SIG_IGN
、SIG_DFL
。如果出错,则返回 SIG_ERR
实例程序:
使用 signal() 函数来捕捉定时器产生的信号 SIGALRM
1 |
|
(2)sigaction()
sigaction()
函数和 signal()
函数的功能是一样的,用于捕捉进程中产生的信号,并将用户自定义的信号行为函数(回调函数)注册给内核,内核在信号产生之后调用这个处理动作。sigaction()
可以看做是 signal() 函数是加强版,函数参数更多更复杂,函数功能也更强一些。函数原型如下:
1 |
|
参数:
signum
: 要捕捉的信号act
: 捕捉到信号之后的处理动作oldact
: 上一次调用该函数进行信号捕捉设置的信号处理动作, 该参数一般指定为NULL
返回值:函数调用成功返回0,失败返回-1
(3)sigaction结构体
sigaction
函数的参数是一个结构体类型,结构体原型如下:
1 | struct sigaction { |
结构体成员介绍:
sa_handler
: 函数指针,指向的函数就是捕捉到的信号的处理动作sa_sigaction
: 函数指针,指向的函数就是捕捉到的信号的处理动作,有三个参数,可以获得关于信号更详细的信息sa_mask
: 在信号处理函数执行期间, 临时屏蔽某些信号, 将要屏蔽的信号设置到集合中即可- 当前处理函数执行完毕, 临时屏蔽自动解除
- 假设在这个集合中不屏蔽任何信号, 默认也会屏蔽一个(捕捉的信号是谁, 就临时屏蔽谁)
sa_flags
:使用函数指针指向的函数处理捕捉到的信号(指定信号处理的行为)0
:使用sa_handler
(一般情况下使用这个)SA_SIGINFO
:使用sa_sigaction
成员而不是sa_handler
作为信号处理函数SA_RESTART
,使被信号打断的系统调用自动重新发起SA_NOCLDSTOP
,使父进程在它的子进程暂停或继续运行时不会收到SIGCHLD
信号SA_NOCLDWAIT
,使父进程在它的子进程退出时不会收到SIGCHLD
信号,这时子进程如果退出也不会成为僵尸进程SA_NODEFER
,使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号SA_RESETHAND
,信号处理之后重新设置为默认的处理方式
sa_restorer
: 被废弃的成员
(4)SIGCHLD信号
当子进程退出、暂停、从暂停回复运行的时候,在子进程中会产生一个SIGCHLD
信号,并将其发送给父进程,但是父进程收到这个信号之后默认就忽略了。我们可以在父进程中对这个信号加以利用,基于这个信号来回收子进程的资源,因此需要在父进程中捕捉子进程发送过来的这个信号
(5)SIGALRM、SIGTERM信号
1 |
4.常用函数
(1)alarm()
这个函数用于为调用进程设置一个实时闹钟,当这个闹钟超时时,系统会向该进程发送一个SIGALRM
信号。它定义在<unistd.h>
头文件中
1 |
|
参数
seconds
:设置的超时时间,单位为秒。当设置为0
时,任何之前设置的闹钟都会被取消
返回值
- 如果之前已经设置了一个闹钟,且它尚未超时,那么
alarm
函数会返回之前那个闹钟的剩余时间(秒)。否则,它返回0
当alarm
设置的时间到达时,操作系统会向进程发送SIGALRM
信号。默认情况下,这会终止进程。但你可以使用signal
或sigaction
函数来定义自己的处理函数,从而在收到SIGALRM
信号时执行特定的操作
SIGALRM
默认行为:当进程接收到SIGALRM
信号且没有为它设置处理程序时,默认的行为是终止进程SIGALRM
自定义处理:你可以使用signal
或sigaction
函数为SIGALRM
定义自己的处理程序,这样你就可以在接收到信号时执行特定的操作,而不是让进程终止
(2)setitimer()
setitimer
是用于设置间隔计时器的函数。与之相关的是 getitimer
,用于获取当前设置的计时器的值
该函数时间精度很高,而且误差不会累积,因此setitimer
比alarm
函数更好
函数原型
1 |
|
参数:
which
: 指定要设置的计时器的类型,通常可以是:ITIMER_REAL
: 实时计时器,当计时器超时时,将发送SIGALRM
信号。ITIMER_VIRTUAL
: 虚拟计时器,仅在进程执行时减少。ITIMER_PROF
: 类似于ITIMER_VIRTUAL
,但还包括了当系统执行 behalf 时的时间。
new_value
: 指定新计时器的间隔和值。old_value
: 如果不为NULL
,那么在调用之前计时器的当前值将存储在此处
itimerval
结构体的定义大致如下:
1 | struct itimerval { |
当it_value
递减为0
时,发信号。并且it_interval
会原子化赋值给it_value
,重新开始计时
其中,timeval
结构如下:
1 | struct timeval { |
返回值
- 0成功,-1表示失败
使用 setitimer
时,还需要处理可能由计时器超时产生的信号,如 SIGALRM
注意事项:
定时器类型限制
- 每种类型的定时器(
ITIMER_REAL
、ITIMER_VIRTUAL
、ITIMER_PROF
)在任何时刻都只能有一个活动实例。因此,对于每种类型的定时器,后续的setitimer
调用会重置该类型的定时器设置- 如果你调用
setitimer
设置了一个ITIMER_REAL
定时器,然后再次调用setitimer
设置同类型的定时器,第二个调用会覆盖第一个调用的设置
5.其他函数
(1)socketpair()
在linux
下,使用socketpair
函数能够创建一对匿名套接字进行通信,这两个套接字可以用于进程间通信,项目中使用管道通信
1 |
|
参数:
domain
:协议族,通常使用AF_UNIX
(表示 Unix 域套接字)。
type
:套接字类型,常见的值包括:
SOCK_STREAM
:面向连接的流套接字SOCK_DGRAM
:无连接的数据报套接字
protocol
:通常设置为 0,表示使用默认协议
sv[2]
:整数数组,用于存储返回的两个套接字文件描述符。
返回值:
成功时返回 0,并在
sv
中存储两个套接字文件描述符失败时返回 -1,并设置
errno
以指示错误
**socketpair与pipe的区别
**:
**
socketpair
**:
- 函数创建一对相互连接的套接字。这些套接字之间的通信是全双工的,即数据可以在两个方向上同时流动
**
pipe
**:
- 函数创建一个单向数据通道,数据只能从一端(写端)流向另一端(读端)。这意味着使用
pipe
创建的管道仅支持半双工通信,通常需要两个管道来实现双向通信(一个用于每个方向)
实例程序,用于父子进程通信
1 |
|
(2)send()
send
函数用于通过套接字发送数据,它通常用于 TCP 套接字(流套接字)。这个函数可以发送数据到连接的另一端
1 |
|
参数:
sockfd
:套接字文件描述符,标识一个已连接的套接字
buf
:指向要发送的数据缓冲区的指针
len
:要发送的数据的长度(字节数)
flags
:发送选项,可以是以下值的组合(或 0)
MSG_CONFIRM
:确认路径有效MSG_DONTROUTE
:不使用路由表,直接发送到网络接口MSG_DONTWAIT
:非阻塞模式发送数据MSG_EOR
:指示数据结束MSG_MORE
:数据未完,后续还有数据MSG_NOSIGNAL
:如果对等方关闭连接,不发送 SIGPIPE 信号
返回值:
成功时返回发送的字节数
失败时返回 -1,并设置
errno
以指示错误
当套接字发送缓冲区变满时,send
通常会阻塞,除非套接字设置为非阻塞模式,当缓冲区变满时,返回EAGAIN
或者EWOULDBLOCK
错误,此时可以调用select
函数来监视何时可以发送数据
三、信号通知流程
Linux
下的信号采用的异步处理机制,信号处理函数和当前进程是两条不同的执行路线。具体的,当进程收到信号时,操作系统会中断进程当前的正常流程,转而进入信号处理函数执行操作,完成后再返回中断的地方继续执行
为避免信号竞态现象发生,信号处理期间系统不会再次触发它。所以,为确保该信号不被屏蔽太久,信号处理函数需要尽可能快地执行完毕
一般的信号处理函数需要处理该信号对应的逻辑,当该逻辑比较复杂时,信号处理函数执行时间过长,会导致信号屏蔽太久
这里的解决方案是,信号处理函数仅仅发送信号通知程序主循环,将信号对应的处理逻辑放在程序主循环中,由主循环执行信号对应的逻辑代码
1.统一事件源
统一事件源,是指将信号事件与其他事件一样被处理
具体的,信号处理函数使用管道将信号传递给主循环,信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出信号值,使用I/O
复用系统调用来监听管道读端的可读事件,这样信号事件与其他文件描述符都可以通过epoll
来监测,从而实现统一处理
2.信号处理机制
每个进程之中,都有存着一个表,里面存着每种信号所代表的含义,内核通过设置表项中每一个位来标识对应的信号类型
信号的接收
- 收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的
信号的检测
- 进程从内核态返回到用户态前进行信号检测
- 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测
- 进程陷入内核态后,有两种场景会对信号进行检测:
- 当发现有新信号时,便会进入下一步,信号的处理
信号的处理
- ( 内核 )信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(
eip
)将其指向信号处理函数- ( 用户 )接下来进程返回到用户态中,执行相应的信号处理函数
- ( 内核 )信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理
- ( 用户 )如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(
eip
)将其指向中断前的运行位置,最后回到用户态继续执行进程
至此,一个完整的信号处理流程便结束了,如果同时有多个信号到达,上面的处理流程会在第2步和第3步骤间重复进行
四、代码分析
1.信号处理函数
(1)sig_handler()
自定义信号处理函数,创建sigaction
结构体变量,设置信号函数
1 | /* |
信号处理函数中仅仅通过管道发送信号值,不处理信号对应的逻辑,缩短异步执行时间,减少对主程序的影响
注意:在信号处理函数中保留和恢复 errno
是一个常见的实践,目的是确保信号处理函数不会意外地修改当前执行环境中的错误状态,从而避免对程序的正常运行产生不良影响
errno
的作用:
errno
是一个全局变量,用于指示最近一次系统调用或库函数调用的错误代码。许多库函数和系统调用在遇到错误时会设置errno
,而程序通常会检查errno
以确定错误的具体原因信号处理函数特点:
- 异步调用:信号处理函数可以在程序执行的任何时刻被调用,可能会打断当前正在执行的代码,包括正在执行的系统调用或库函数
- 快速返回:信号处理函数应该尽量简短,只做最必要的工作,因为它们是异步执行的,会打断程序的正常运行
保留与恢复errno
的原因:
防止
errno
被覆盖
- 当信号处理函数被调用时,当前程序可能正在进行系统调用或库函数调用,并且这些调用可能会设置
errno
- 如果信号处理函数内部调用了会修改
errno
的系统调用或库函数,那么会覆盖原来的errno
值,导致信号处理函数返回后,程序无法正确地识别之前的错误状态- 理解:若我们在进行系统调用时,设置了
errno
,并且我们可以根据errno
进行一些日志输出,但是在我们还未进行日志输出之前,就捕获到了信号处理函数,对应的信号,产生中断进而调用执行了任务处理函数,在任务处理函数,若存在相关函数将当前程序的errno
进行了更改。那么当任务处理函数执行结束,当前程序的errno
与之前进行系统调用时,设置的errno
就存在差异,我们根据当前errno
进行的日志输出就会有问题确保程序的正确性
- 程序中的很多部分可能依赖于
errno
的值来判断操作是否成功或失败- 信号处理函数在执行过程中修改
errno
可能会干扰这些判断,导致程序行为异常
(2)addsig()
1 | /* |
项目中设置信号函数,仅关注SIGTERM
和SIGALRM
两个信号
2.信号通知逻辑
创建管道,其中管道写端写入信号值,管道读端通过
I/O
复用系统(epoll IO
复用)监测读事件设置信号处理函数
SIGALRM
(时间到了触发)和SIGTERM
(kill
会触发,Ctrl+C
)
- 通过
struct sigaction
结构体和sigaction
函数注册信号捕捉函数- 在结构体的
handler
参数设置信号处理函数,具体的,从管道写端写入信号的名字(需要处理的信号)利用
I/O
复用系统监听管道读端文件描述符的可读事件信息值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码
(1)代码分析
1 | // socketpair()函数用于创建一对无名的、相互连接的管道套接字 |
(2)进一步分析
为什么管道写端要非阻塞?
send是将信息发送给套接字缓冲区,如果缓冲区满了,则会阻塞,这时候会进一步增加信号处理函数的执行时间,为此,将其修改为非阻塞
没有对非阻塞返回值处理,如果阻塞是不是意味着这一次定时事件失效了?
是的,但定时事件是非必须立即处理的事件,可以允许这样的情况发生
管道传递的是什么类型?switch-case的变量冲突?
信号本身是整型数值,管道中传递的是ASCII码表中整型数值对应的字符
switch
的变量一般为字符或整型,当switch
的变量为字符时,case
中可以是字符,也可以是字符对应的ASCII
码