赞
踩
目录
4.5.1 sigprocmask:更改或检查进程的信号屏蔽字。
4.5.2 sigpending:检查当前进程的未决信号集。
4.5.3 sigemptyset:将信号集初始化为空集,即清除信号集中所有的信号,使其不包含任何信号。
4.5.4 sigfillset:将信号集初始化为满集,即包含所有可能的信号。
4.5.5 sigaddset:将指定的信号添加到信号集中。
4.5.6 sigdelset:将指定的信号从信号集中删除。
4.5.7 sigismember:检查指定的信号是否在信号集中。
想象你在家里等待一个快递包裹的到来:
快递员到达通知:
Ctrl+C
时,系统会产生一个SIGINT
信号,发送给当前正在运行的前台进程。你正在忙碌:
处理快递:
SIGINT
的默认操作是终止进程,但你可以自定义一个处理函数来执行其他动作。可以在终端使用 kill -l 查看信号的种类:
标准信号是由POSIX标准定义的,所有Unix和Linux系统都支持这些信号。每个信号都有一个固定的编号和对应的宏定义名称。
信号在Linux系统中是一种用于进程间通信和事件通知的机制。信号的产生可以由多种途径触发,具体包括以下几种方式:
Ctrl+C
产生,用于终止前台进程。Ctrl+\
产生,用于终止进程并产生核心转储(core dump)。abort()
函数产生。alarm()
函数设定的计时器到期时产生。这些信号编号在1到31之间,涵盖了大多数常见的进程控制和错误处理机制。
实时信号的编号范围通常从34开始,根据具体的Linux实现可能有所不同。这些信号是POSIX实时扩展的一部分,提供了更高的优先级和实时性特性。
这里不做过多说明。
用户可以通过在终端按特定的键来产生信号。例如:
Ctrl+C
时,系统会产生一个SIGINT
信号并发送给当前运行的前台进程。这个信号的默认处理动作是终止进程。Ctrl+\
时,系统会产生一个SIGQUIT
信号,默认处理动作是终止进程并产生一个核心转储(core dump)在Linux和其他类Unix操作系统中,signal
是一个函数,用于设置进程对特定信号的处理方式。信号(signal)是进程间通信的一种机制,用于通知进程某个事件的发生。
signal
函数用于指定某个信号的处理函数。当进程接收到该信号时,操作系统会调用指定的处理函数。通过这个函数,程序可以定义自定义的行为来响应信号,而不仅仅是执行默认的处理动作(比如终止进程)。
其函数原型如下:
#include <signal.h> void (*signal(int sig, void (*func)(int)))(int);
sig
: 指定的信号编号(例如SIGINT
表示中断信号)。func
: 指向信号处理函数的指针。处理函数接受一个整数参数,该参数是信号的编号。
以下是一个demo,可以用来测试用户通过终端产生信号 。
- #include <stdio.h>
- #include <signal.h>
- #include <unistd.h>
-
- // 自定义的信号处理函数
- void handle_sigint(int sig)
- {
- printf("Caught signal %d (SIGINT). Exiting...\n", sig);
- _exit(0);
- }
-
- int main()
- {
- // 设置对 SIGINT 信号的处理函数
- signal(SIGINT, handle_sigint);
- // 无限循环,等待信号到来
- while (1)
- {
- printf("Running... Press Ctrl+C to stop.\n");
- sleep(1);
- }
- return 0;
- }
- /*signal(SIGINT, handle_sigint): 设置对 SIGINT 信号的处理函数为 handle_sigint。当进程接收到 SIGINT 信号时,将调用 handle_sigint 函数。
- handle_sigint 函数:自定义的信号处理函数,打印接收到的信号编号。*/
demo 解释:
signal(SIGINT, handle_sigint)
: 设置对SIGINT
信号的处理函数为handle_sigint
。当进程接收到SIGINT
信号时,将调用handle_sigint
函数。handle_sigint
函数:自定义的信号处理函数,打印接收到的信号编号。
可见,CTRL + c 就是信号 SIGINT ,按下 CTRL + c 后,系统进入handle函数,执行了 exit
如果对 demo 进行修改,取消掉 exit 后,再按下 CTRL + c 后,程序就不会终止了。
产生这种现象的原因是 signal 函数改变了对 CTRL + c 这种信号的处理方式,把退出的处理方式修改成了 handle_sigint 函数中的方式。
程序可以通过调用系统提供的函数来产生信号,例如:
kill(pid, signo)
,其中pid
是目标进程的进程ID,signo
是要发送的信号编号。kill(getpid(), signo)
。SIGABRT
信号并异常终止。- #include <stdio.h>
- #include <signal.h>
- #include <unistd.h>
-
- // 自定义的信号处理函数
- void handle_sigusr1(int sig)
- {
- printf("Caught signal %d (SIGUSR1)\n", sig);
- _exit(0);
- }
-
- int main()
- {
- // 设置对 SIGUSR1 信号的处理函数
- signal(SIGUSR1, handle_sigusr1);
- pid_t pid = fork();
-
- if (pid == 0)
- {
- // 子进程
- sleep(2);
- kill(getppid(), SIGUSR1); // 向父进程发送 SIGUSR1 信号
- _exit(0);
- }
- else
- {
- // 父进程
- while (1)
- {
- printf("Waiting for SIGUSR1 from child process...\n");
- sleep(1);
- }
- }
-
- return 0;
- }
3.2.2 demo 现象
通过上述 demo ,子进程在休息2s后会对父进程发送信号然后退出,父进程进入一个无限循环,等待 SIGUSR1
信号。当信号到达时,信号处理函数 handle_sigusr1
将被调用。
当 handle_sigusr1
被调用时,会打印出 Caught signal %d (SIGUSR1)\n ,然后执行 _exit ,退出程序。
一些信号是由特定的软件条件触发的,例如:
SIGPIPE
信号。alarm(seconds)
函数设定一个闹钟,在指定的秒数后系统会向当前进程发送SIGALRM
信号。函数原型
#include <unistd.h> unsigned int alarm(unsigned int seconds);
seconds
:指定计时器的秒数。设定计时器在seconds
秒之后发送SIGALRM
信号。如果seconds
为 0,表示取消任何现有的计时器。- 返回先前设定的闹钟时间还剩余的秒数。如果没有设定过闹钟,则返回 0。
- #include <stdio.h>
- #include <signal.h>
- #include <unistd.h>
-
- // 自定义的信号处理函数
- void handle_sigalrm(int sig)
- {
- printf("Caught signal %d (SIGALRM). Time's up!\n", sig);
- }
-
- int main()
- {
- // 设置对 SIGALRM 信号的处理函数
- signal(SIGALRM, handle_sigalrm);
- alarm(5); // 设定闹钟在 5 秒后触发 SIGALRM 信号
-
- // 无限循环,等待信号到来
- while (1)
- {
- printf("Sleeping... Waiting for alarm...\n");
- sleep(1);
- }
-
- return 0;
- }
-
首先使用 alarm 函数设定了一个5s后的闹钟,程序会在5s后接收到 SIGALRM 信号,同时使用signal 函数重新设计了 SIGALRM 信号的处理方式,所以执行程序后会看到以下现象:
在Unix和类Unix系统中,alarm
函数只支持设置一个定时器。如果在一个进程中设置了两个alarm
调用,后面的调用会覆盖前面的调用。
具体来说,当你第二次调用alarm
时,它会取消前一个定时器并重新设定一个新的定时器。因此,第一个定时器所关联的SIGALRM
信号将不会被发送,只有最后一次调用alarm
设置的定时器到期时,才会发送SIGALRM
信号。
当进程执行非法操作(如除以0或者页表对应失败(数组越界、野指针...))时,硬件会产生异常,内核将这些异常转换为信号并发送给进程,例如:
SIGSEGV
信号,通常导致进程异常终止。SIGFPE
信号- #include <stdio.h>
- #include <signal.h>
- #include <unistd.h>
-
- // 自定义的信号处理函数
- void handle_sigfpe(int sig)
- {
- printf("Caught signal %d (SIGFPE). Division by zero!\n", sig);
- _exit(1);
- }
-
- int main()
- {
- // 设置对 SIGFPE 信号的处理函数
- signal(SIGFPE, handle_sigfpe);
- int x = 1;
- int y = 0;
- int z = x / y; // 这将导致 SIGFPE 信号
- printf("Result: %d\n", z);
- return 0;
- }
父进程和子进程之间可以通过信号进行通信。例如,当子进程终止时,会向父进程发送SIGCHLD
信号。父进程可以捕捉并处理该信号,以便执行相应的清理工作,避免产生僵尸进程。
SIGCHLD
是一个特定的信号,用于通知父进程其子进程的状态变化。通常,当一个子进程终止或停止时,系统会向父进程发送 SIGCHLD
信号。父进程可以通过捕捉和处理 SIGCHLD
信号来得知其子进程的终止或停止状态,并进行相应的处理,如清理资源或重新启动子进程。
以下是一个子进程对应一个父进程时,子进程退出向父进程发出 SIGCHLD 信号。
- #include <stdio.h>
- #include <stdlib.h>
- #include <signal.h>
- #include <sys/wait.h>
- #include <unistd.h>
-
- // 自定义的信号处理函数
- void handle_sigchld(int sig)
- {
- pid_t pid;
- int status;
- while ((pid = waitpid(-1, &status, WNOHANG)) > 0)
- {
- printf("Child %d terminated\n", pid);
- }
- }
-
- int main()
- {
- // 设置对 SIGCHLD 信号的处理函数
- signal(SIGCHLD, handle_sigchld);
-
- if (fork() == 0)
- {
- // 子进程
- printf("Child process: %d\n", getpid());
- sleep(2);
- _exit(0);
- }
- else
- {
- // 父进程
- while (1)
- {
- printf("Parent process doing some work...\n");
- sleep(1);
- }
- }
-
- return 0;
- }
这里对SIGCHLD信号进行处理,在处理方式中设置了 waitpid 的方法,同时,其中设置了WNOHANG 的方式,防止子进程一部分退出另一部分不退出造成的进程堵塞,这样也会导致父进程无法进行自己的操作。
同时,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用 sigaction 将SIGCHLD 的处理动作置为 SIG_IGN ,这样 fork 出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略:
signal(SIGCHLD, SIG_IGN);
Ctrl+C
、调用kill
函数、硬件异常等),内核会为目标进程产生一个信号。信号在一个进程的生命周期中可以有三种状态:
每个进程都有两个重要的数据结构用于信号的管理:
当一个信号递达时,内核会根据以下步骤处理信号:
sigset_t
类型的信号集- #include <signal.h>
- int sigemptyset(sigset_t *set);
- int sigfillset(sigset_t *set);
- int sigaddset (sigset_t *set, int signo);
- int sigdelset(sigset_t *set, int signo);
- int sigismember(const sigset_t *set, int signo);
函数原型
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how
:指定如何修改信号屏蔽字(如SIG_BLOCK
阻塞信号)。set
:指向要设置的信号集。oldset
:如果不为 NULL,保存先前的信号屏蔽字。
函数原型
int sigpending(sigset_t *set);
set
:指向保存未决信号集的信号集。
函数原型
int sigemptyset(sigset_t *set);
set
:指向要初始化的信号集。- 成功时返回 0;出错时返回 -1。
函数原型
int sigfillset(sigset_t *set);
set
:指向要初始化的信号集- 成功时返回 0;出错时返回 -1。
函数原型
int sigaddset(sigset_t *set, int signo);
set
:指向要修改的信号集。signo
:要添加到信号集中的信号编号。- 成功时返回 0;出错时返回 -1。
函数原型
int sigdelset(sigset_t *set, int signo);
set
:指向要修改的信号集。signo
:要从信号集中删除的信号编号。- 成功时返回 0;出错时返回 -1。
函数原型
int sigismember(const sigset_t *set, int signo);
set
:指向要检查的信号集。signo
:要检查的信号编号。- 如果信号在信号集中,返回 1;如果不在信号集中,返回 0;出错时返回 -1。
- #include <iostream>
- #include <unistd.h>
- #include <cstdio>
- #include <sys/types.h>
- #include <sys/wait.h>
-
- void PrintPending(sigset_t &pending)//打印“位图”
- {
- std::cout << "curr process[" << getpid() << "]pending: ";
- for (int signo = 31; signo >= 1; signo--)
- {
- if (sigismember(&pending, signo))
- {
- std::cout << 1;
- }
- else
- {
- std::cout << 0;
- }
- }
- std::cout << "\n";
- }
-
- void handler(int signo)
- {
- std::cout << signo << " 号信号被递达!!!" << std::endl;
- }
-
- int main()
- {
- // 0. 捕捉2号信号
- signal(2, handler); // 自定义捕捉
- signal(2, SIG_IGN); // 忽略一个信号
- signal(2, SIG_DFL); // 信号的默认处理动作
-
- // 1. 屏蔽2号信号
- sigset_t block_set, old_set;
- sigemptyset(&block_set);
- sigemptyset(&old_set);
- sigaddset(&block_set, SIGINT);
- // 1.1 设置进入进程的Block表中
- sigprocmask(SIG_BLOCK, &block_set, &old_set); // 真正的修改当前进行的内核block表,完成了对2号信号的屏蔽!
-
- int cnt = 10;
- while (true)
- {
- // 2. 获取当前进程的pending信号集
- sigset_t pending;
- sigpending(&pending);
-
- // 3. 打印pending信号集
- PrintPending(pending);
- cnt--;
-
- // 4. 解除对2号信号的屏蔽
- if (cnt == 0)
- {
- std::cout << "解除对2号信号的屏蔽!!!" << std::endl;
- sigprocmask(SIG_SETMASK, &old_set, &block_set);
- }
-
- sleep(1);
- }
- }
通过前面的学习,已经了解了我们可以自定义信号处理的方式,当对某信号进行自定义处理时,系统就要去找自定义的 handler 处理方法,但是,系统拥有最高的权限,它的这种身份被称作内核态,普通用户则被成为用户态。系统会以内核态的方式直接去执行自定义的 handler 函数吗?很显然是不行的。
这样如果某个用户钻了漏子,借用系统内核态的身份完成一些用户态不可以完成的事情,就会惹到麻烦。操作系统在这时就存在着身份的转换。
下面以32位机器为例:
4G的内存中,0-3G是供用户使用的,3-4G是操作系统的所有代码和数据。当用户想访问[3-4]G的地址时,只能使用系统调用!
操作系统也是一个软件,它是第一个加载到内存的软件,它的页表只会维护一份,所以当从用户级换到内核级时,无论在哪个进程,相应的系统调用会访问内核地址空间,映射到同一个内核级页表,进而每个进程进入的OS内部都是相同的!
系统调用访问内核地址空间:无论哪个进程发起系统调用,都会进入相同的内核地址空间,访问相同的内核数据结构和代码。
映射到相同的内核级页表:每个进程在进入内核态时,使用的都是相同的内核级页表。这确保了内核环境的一致性和简化了内存管理。
统一的OS内部环境:由于共享相同的内核地址空间和内核级页表,每个进程进入内核态时,看到的OS内部环境是相同的。
信号的产生(进入内核态):
Ctrl+C
或硬件异常)触发信号时,内核会生成该信号并将其标记为待处理状态。此时,进程会从用户态切换到内核态。kill
或raise
)产生的,同样会引发进程进入内核态。信号的检查与处理准备(进入用户态):
信号处理函数的执行(进入内核态):
信号处理函数的完成(进入内核态并返回用户态):
- 用户态 (User Mode)
- |
- | (事件触发,如 Ctrl+C)
- V
- 内核态 (Kernel Mode)
- |
- | (信号生成,标记待处理)
- V
- 内核态 (Kernel Mode)
- |
- | (准备信号处理)
- V
- 用户态 (User Mode)
- |
- | (执行信号处理函数)
- V
- 内核态 (Kernel Mode)
- |
- | (信号处理函数执行完毕)
- V
- 用户态 (User Mode)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。