信号
信号(signal)机制是Linux系统中最为古老的进程之间的通信机制。Linux信号也可以称为软中断,是在软件层次上对中断机制的一种模拟。在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,进程不需要通过任何操作等待信号到达。
信号来源
- 硬件来源:硬件信号触发,硬件异常,如除零运算,内存非法访问等。
- 软件来源:系统函数 kill() raise() alarm() 和 setitimer() 等函数,ctl+c 发出 SIGINT 等
信号分类
Linux系统中定义了一系列的信号,总共64种,分为非实时信号和实时信号。
- 非实时信号:编号范围是从 1 到 31。这些标准信号是由操作系统内核发出,用于通知进程发生了某种事件或错误。
- 实时信号:编号范围是从 32 到 64。有如下特点:
- 实时信号可以排队传递,当多个相同类型的实时信号同时到达时,系统会将其排队传递给进程,直到进程处理完所有实时信号。
- 实时信号的优先级高于标准信号,进程在接收实时信号时会暂时把标准信号放入挂起状态。
- 实时信号支持使用 sigqueue() 函数向目标进程发送带有额外数据的信号。
可以使用 kill ‐l 命令查看所有的信号。
前31种信号含义如下:
- SIGHUP(1):当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作是终止进程。
- SIGINT(2):当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作是终止进程。
- SIGQUIT(3):当用户按下<Ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作是终止进程。
- SIGILL(4):非法指令信号,当一个进程执行了一条非法的、不支持的或者无效的指令时,操作系统会向该进程发送SIGILL信号。默认动作是终止进程并产生core文件。
- SIGTRAP(5):该信号由断点指令或其他trap指令产生,当一个进程执行了一个与调试器相关的操作(打断点)时,操作系统会向该进程发送SIGTRAP信号。默认动作为终止里程并产生core文件。
- SIGABRT(6):调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
- SIGBUS(7):非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
- SIGFPE(8):在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认 动作为终止进程并产生core文件。
- SIGKILL(9):无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
- SIGUSE1(10):用户定义的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
- SIGSEGV(11):指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。
- SIGUSR2(12):这是另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
- SIGPIPE(13): 管道破裂信号,向一个没有读端的管道写数据。默认动作为终止进程。
- SIGALRM(14):定时器超时,超时的时间由系统调用alarm设置。默认动作为终止进程。
- SIGTERM(15):程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来告示程序正常退出。执行 kill 命令时,缺省产生这个信号。默认动作为终止进程。
- SIGSTKFLT(16):段错误信号,当一个进程访问了无效的内存地址或者试图对不可写的内存进行写操作时,会产生该信号。默认动作为终止进程并产生core文件。
- SIGCHLD(17):子进程状态(终止、暂停、继续)发生变化时,父进程会收到这个信号。默认动作为忽略这个信号。
- SIGCONT(18):用于通知一个被停止(暂停)的进程继续执行。
- SIGSTOP(19):用于暂停(停止)一个正在执行的进程,本信号不能被忽略,处理和阻塞。
- SIGTSTP(20):当用户按下<Ctrl+Z>组合键时产生该信号,用于将一个进程暂停(停止)执行并放入后台。
- SIGTTIN(21):当一个进程在后台运行,并尝试从终端读取输入时,如果该终端处于非控制状态(即不是当前正在与之交互的终端),那么该进程将会被操作系统发送SIGTTIN信号,以提示它正在尝试从非控制终端读取输入。默认动作为暂停进程。
- SIGTTOU(22):类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
- SIGURG(23):套接字上有紧急数据时,向当前正在运行的进程发送该信号,报告有紧急数据到达。默认动作为忽略该信号。
- SIGXCPU(24):表示进程已经超过了它被允许的CPU时间限制。当一个进程超过了被操作系统设定的CPU时间限制时,操作系统会向该进程发送SIGXCPU信号。这通常发生在进程占用了太多的CPU时间或者执行了一个长时间运行的计算任务。默认动作为终止进程。
- SIGXFSZ(25):超过文件的最大长度设置。默认动作为终止进程。
- SIGVTALRM(26):按照进程在用户态占用的CPU时间定时,默认动作为终止进程。
- SIGPROF(27):按照进程在用户态和内核态占用的CPU时间定时,默认动作为终止进程。
- SIGWINCH(28):表示窗口尺寸已更改,当用户调整终端窗口的大小时,会触发此信号。默认动作为忽略该信号。
- SIGIO(29):此信号向进程指示发出了一个异步IO事件。默认动作为忽略。
- SIGPWR(30):关机。默认动作为终止进程。
- SIGSYS(31):无效的系统调用。默认动作为终止进程并产生core文件。
信号传递和响应
在 Linux 中,信号的传递流程通常遵循以下步骤:
- 信号产生:信号可以由内核、操作系统或者应用程序生成。常见的信号产生方式包括用户在终端按下键盘快捷键产生的信号,以及系统调用执行过程中出现的异常情况。
- 信号传递:一旦信号被产生,内核会把信号传递给目标进程。目标进程可以是正在等待信号的进程,或者正在执行的进程。如果信号没有被阻塞并且未被忽略,内核将会将信号传递给进程。
- 信号处理:一旦进程接收到信号,内核会检查信号的处理方式。根据注册的信号处理方式,可以有以下几种情况:
- 忽略信号:即对信号不做任何处理,但是两个信号不能忽略: SIGKILL 以及 SIGSTOP 。
- 捕捉信号:当信号发生时,执行用户定义的信号处理函数。
- 执行默认操作:Linux对每种信号都规定了默认操作,man 7 signal查看。
- 信号处理过程:如果信号的处理方式是执行自定义的信号处理函数,进程将会调用该函数来处理信号。处理函数可以完成一系列操作,比如修改全局变量、执行清理操作等。在信号处理函数执行期间,进程可能会被阻塞在信号处理函数中。
- 信号返回:一旦信号处理函数执行完毕,程序将会从信号处理函数中返回,程序恢复执行之前的代码。
注:信号传递不支持排队,当在很短时间内传递多个相同信号,该信号只会被处理一次。
在 Linux 中,信号的传递流程通常是由内核负责进行管理和调度,确保信号被准确传递给目标进程,并按照指定的处理方式进行处理。对于开发人员来说,可以通过信号处理函数来实现对信号的处理和响应,以应对不同的信号产生场景。
sigset_t类型
信号集是信号的集合,用sigset_t类型描述。每一种信号用1bit来表示,前面我们提到信号有64种,那么这个sigset_t类型至少占64bit。当有信号传递到该进程的时候,未决信号集中的对应位设置为1,其他位保持不变。之后,信号传递到信号屏蔽字,信号屏蔽字中为1的信号,将被阻塞,其他为0且未决的信号,将交由进程的信号处理函数负责处理。
通过维护未决信号集和信号屏蔽字,进程能够控制接收到的信号以及在处理信号时需要被阻塞的信号,确保信号能够按照预期方式处理,同时避免因处理信号而引起的竞争条件或意外中断问题。
sigset_t 数据类型提供了一些函数来操作信号集合:
#include <signal.h>
//将set每一位都置0
int sigemptyset(sigset_t *set)
//将set每一位都置1
int sigfillset(sigset_t *set)
//将set中signo信号对应的bit置1
int sigaddset(sigset_t *set, int signo)
//将set中signo信号对应的bit置0
int sigdelset(sigset_t *set, int signo)
//判断set中signo信号对应bit是否为1,返回1或者0。
int sigismember(const sigset_t *set, int signo)
未决信号集
未决信号集是指进程当前已经接收到但尚未被处理的信号的集合。当进程接收到一个信号时,该信号会被添加到进程的未决信号集中,直到进程处理完该信号或将其阻塞。
可以通过系统调用sigpending()函数来获取当前进程的未决信号集,该函数定义如下:
#include <signal.h>
int sigpending(sigset_t *set);
参数说明
返回值
调用成功则返回0,出错则返回‐1。
信号屏蔽字
信号屏蔽字是一个掩码,用于指定在信号处理期间需要被阻塞的信号集合。当某个信号被加入到信号屏蔽字中时,该信号在信号处理期间将被阻塞,即不会被传递给进程。
可以通过系统调用sigprocmask()函数来查看或更改进程的信号屏蔽字。该函数定义如下:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数说明
- how:指定了函数的操作,有如下取值:
- SIG_BLOCK:将 set 中指定的信号添加到当前信号屏蔽字中。相当于 mask | set
- SIG_UNBLOCK:将 set 中指定的信号从当前信号屏蔽字中移除。相当于 mask & ~set
- SIG_SETMASK:将当前信号屏蔽字设置为 set 中指定的值。相当于 mask = set
- set:指定要修改的信号集合。
- oldset:用于获取之前的信号屏蔽字。
注:SIGSTOP(19)和 SIGKILL(9)不能被忽略。
返回值
调用成功则返回0,出错则返回‐1。
注:信号屏蔽并不是将未决信号丢弃,而是阻塞该信号。
信号捕获-signal()函数
signal()函数常用于设置信号处理程序,以便进程在接收到特定的信号时执行自定义的处理逻辑。可以通过指定不同的信号和处理函数来实现对各种信号的处理,比如捕获SIGINT信号来处理终端中断信号,或者捕获SIGSEGV信号来处理段错误。该函数定义如下:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)));
参数说明
- signum:要安装的信号值。
- handler:信号对应的信号处理函数。
signal()函数主要用于前32种非实时信号的安装。
信号捕获-sigaction()函数
sigaction()函数和signal()函数相似,相比于前者,sigaction()函数提供了更为灵活和可靠的信号处理方式。该函数定义如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
参数说明
- signum:要安装的信号值。
- act:指向 struct sigaction 类型的结构体指针,用于指定新的信号处理方式。
- oldact:指向 struct sigaction 类型的结构体指针,用于存储之前的信号处理方式。
返回值
如果函数执行成功,返回值为0。如果函数执行失败,返回值为-1,并且可以通过 errno 变量来获取具体的错误信息。
struct sigaction结构体
- sa_handler:早期的信号捕捉函数。
- sa_sigaction:新添加的捕捉函数,支持传参, 和sa_handler互斥,两者通过 sa_flags 标识来决定采用哪种捕捉函数。
- sa_mask: 在执行捕捉函数期间,屏蔽指定信号,当退出捕捉函数后,还原回原有的阻塞信号集。
- sa_flags:设置信号处理的标志。
- SA_RESTART:如果设置了这个标志,则系统调用被信号中断后会自动重启,而不是返回-1并设置 errno 为 EINTR 。
- SA_SIGINFO:指定使用带有附加信息的信号处理程序,即使用sa_sigaction字段指定的函数。
- SA_NOCLDSTOP:子进程暂停或继续运行时不会产生SIGCHLD信号。
- SA_NODEFER:在信号处理程序执行期间不屏蔽该信号。
- sa_restorer:无含义的保留字段。
SA_RESTART标志
SA_RESTART标志可以重启被打断的系统调用,例如,在之前介绍read()函数时,该函数被信号打断时,会设置 errno 为EINTR。下面程序模拟被打断的情况:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4 #include<errno.h>
5
6 void signalHandler(int sig)
7 {
8 printf("recv signal : %d\n", sig);
9 }
10
11 int main(int argc, char** argv)
12 {
13 struct sigaction sigact;
14 //sigact.sa_flags = SA_RESTART;
15 sigact.sa_handler = signalHandler;
16 sigemptyset(&sigact.sa_mask);
17
18 sigaction(SIGINT, &sigact, NULL);
19
20
21 char buf[32];
22 int nRet = read(STDIN_FILENO, buf, sizeof(buf));
23 if(nRet == -1)
24 {
25 if(errno == EINTR)
26 printf("read interrupted!\n");
27 }
28
29 return 0;
30 }
输出:
^Crecv signal : 2 //ctrl + c 发送 SIGINT 信号
read interrupted!
为了防止被信号中断,可以封装read函数,之前已经介绍过。也可以使用 SA_RESTART 标志重启中断,例如:
sigact.sa_flags = SA_RESTART;
增加下面代码后,输出如下:
等待信号-pause() 函数
pause() 函数的作用是使当前进程进入睡眠状态,直到接收到一个信号为止。该函数定义如下:
#include <unistd.h>
int pause(void);
返回值
当进程收到一个信号,pause() 函数会返回 -1,并将 errno 设置为 EINTR。
用法:
1 #include<stdio.h>
2 #include<signal.h>
3 #include<unistd.h>
4 #include<errno.h>
5
6 void sigHandler(int sig)
7 {
8 printf("sigHandler\n");
9 }
10
11 int main(int argc, char** argv)
12 {
13 signal(SIGINT, sigHandler);
14
15 printf("Begin Pause!\n");
16 int nRet = pause();
17 if(nRet == -1 && errno == EINTR)
18 printf("Recv Signal, End Pause!\n");
19
20 while(1);
21
22 return 0;
23 }
输出:
Begin Pause!
^CsigHandler //发送 SIGINT 信号
Recv Signal, End Pause!
示例1
下面示例展示了再信号处理函数执行期间,屏蔽其他信号:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<signal.h>
5
6 void sigHandler(int sig)
7 {
8 if(sig == SIGHUP)
9 {
10 printf("Handle SIGHUP \n");
11 exit(0);
12 }
13 else if(sig == SIGINT)
14 {
15 sigset_t set, oldSet;
16 sigemptyset(&set);
17 sigemptyset(&oldSet);
18 sigaddset(&set, SIGHUP);
19
20 sigprocmask(SIG_BLOCK, &set, &oldSet);
21
22 for(int i = 0; i < 10; i++)
23 {
24 printf("Handle SIGINT i = %d\n", i);
25 sleep(1);
26 }
27
28 sigprocmask(SIG_UNBLOCK, &oldSet, NULL);
29 }
30 }
31
32 int main(int argc, char** argv)
33 {
34 printf("pid = %d\n", getpid());
35 signal(SIGINT, sigHandler);
36 signal(SIGHUP, sigHandler);
37 while(1);
38 return 0;
39 }
编译启动程序后,使用 Ctrl + c 发送 SIGINT 信号:
pid = 10531
^CHandle SIGINT i = 0
Handle SIGINT i = 1
Handle SIGINT i = 2
Handle SIGINT i = 3
如果此时在另一个终端发送 kill 命令:
最终输出如下:
pid = 10531
^CHandle SIGINT i = 0
Handle SIGINT i = 1
Handle SIGINT i = 2
Handle SIGINT i = 3
...
Handle SIGINT i = 8
Handle SIGINT i = 9
Handle SIGHUP
如果屏蔽代码中的两个 sigprocmask() 函数调用,发送 kill 命令:
pid = 10581
^CHandle SIGINT i = 0
Handle SIGINT i = 1
Handle SIGINT i = 2
Handle SIGHUP
示例2
sigaction函数简单用法。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<signal.h>
5
6 void sigHandler(int sig)
7 {
8 printf("Handle signal %d\n", sig);
9 sleep(10);
10 printf("Handle finish %d\n", sig);
11 }
12
13 int main(int argc, char** argv)
14 {
15 struct sigaction sigact;
16 sigact.sa_flags = 0;
17 sigact.sa_handler = sigHandler;
18 sigemptyset(&sigact.sa_mask);
19 //sigaddset(&sigact.sa_mask, SIGQUIT);
20 sigaction(SIGINT, &sigact, NULL);
21 while(1);
22 return 0;
23 }
输出:
^CHandle signal 2 //ctrl+c发送 SIGINT 信号
^C^C^C^C^C^C //ctrl+c发送多个 SIGINT 信号
Handle finish 2
Handle signal 2
Handle finish 2 //最终仅处理一次,证明信号不会排队
如果在 SIGINT 信号处理期间,发送 SIGQUIT 信号,运行结果如下:
^CHandle signal 2
^\退出 (核心已转储)
如果在处理期间不想被其他信号打扰,可以向 sigact.sa_mask 中添加屏蔽信号。
发送信号的函数
并不是每个进程都可以向其他的进程发送信号,通常进程只能向具有相同 uid 和 gid 的进程发送信号,或向相同进程组中的其他进程发送信号。
kill()函数
kill()函数用于向指定进程发送信号,信号可以是预定义的系统信号,也可以是用户自定义的信号。该函数定义如下:
#include <signal.h>
int kill(pid_t pid, int sig);
参数说明
- pid:目标进程的进程标识符。有下列取值:
- 正整数:表示具体的进程PID。
- 负整数:表示发送信号给进程组ID等于该值的所有进程。
- 0:表示发送信号给调用进程同一进程组的所有进程;
- -1:表示给除了自身以为所有进程发送信号。
- sig:要发送的信号值。
返回值
如果函数执行成功,返回值为0。如果函数执行失败,返回值为-1,并且可以通过 errno 变量来获取具体的错误信息。
常见错误如下:
- EINVAL:传递的信号编号无效。
- ESRCH:目标进程不存在或进程已经终止,处于僵尸状态。
- EPERM:没有向目标进程发送信号的权利。
例如:
1 #include<stdio.h>
2 #include<signal.h>
3 #include<errno.h>
4 #include<string.h>
5 #include<stdlib.h>
6
7 int main(int argc, char** argv)
8 {
9 if(argc != 3)
10 {
11 printf("Usage: %s [signal] [pid]\n", argv[0]);
12 return -1;
13 }
14
15 int nRet = kill(atoi(argv[2]), atoi(argv[1]));
16 if(nRet == -1)
17 {
18 if(errno == EINVAL)
19 printf("pid error!\n");
20 else if(errno == EPERM)
21 printf("permission error!\n");
22 else if(errno == ESRCH)
23 printf("target process does not exist!\n");
24
25 return -1;
26 }
27
28 return 0;
29 }
输出:
./a.out 9 11111111 //发送不存在的pid
target process does not exist!
./a.out 666 10967 //发送不存在的信号
pid error
./a.out 9 10967 //向root进程发送 SIGKILL 信号
permission error!
raise()函数
raise()函数用于向当前进程发送信号,该函数定义如下:
#include <signal.h>
int raise(int sig);
参数说明
返回值
如果函数执行成功,返回值为0。如果函数执行失败,则返回一个非零值。
alarm()函数
alarm()函数用于设置一个定时器,在指定的时间间隔后发送一个 SIGALRM 信号给调用进程。默认情况下,当定时器到达设定的时间时,进程会终止。我们可以通过捕捉 SIGALRM 信号并自定义处理函数,来实现在定时器到达时执行特定的操作,而非终止进程。该函数定义如下:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
参数说明
- seconds:定时器的时间,单位为秒。在指定的 seconds 秒后,将给自己发送一个SIGALRM 信号。当参数 seconds 为0时,将清除当前进程的 alarm 设置。
返回值
调用alarm()函数时,如果进程已经有一个未结束的 alarm,那么旧的 alarm 将被删除,并返回旧的 alarm 的剩余时间。否则 alarm() 函数返回0。
注:
- alarm()信号不会周期响应,在产生一次信号后,需要重新调用 alarm() 函数创建定时器。
- 定时器有且只能有一个,多次设置会覆盖之前的定时设置。
用法:
1 #include<stdio.h>
2 #include<signal.h>
3 #include<unistd.h>
4
5 void sigHandler(int sig)
6 {
7 static int i = 0;
8 printf("alarm signal, i = %d\n", i++);
9 alarm(1); //需要重新激活
10 }
11
12 int main(int argc, char** argv)
13 {
14 signal(SIGALRM, sigHandler);
15 alarm(1);
16
17 while(1);
18 return 0;
19 }
输出:
alarm signal, i = 0
alarm signal, i = 1
alarm signal, i = 2
...
setitimer()函数
setitimer()函数也是用于设置定时器的函数,该函数可以实现更为精确的定时器控制,包括指定定时器的精度、间隔和触发信号。该函数定义如下:
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
struct itimerval {
struct timeval it_interval; /*每隔多少秒发送一次信号 */
struct timeval it_value; /* 第一次定时时间 */
};
struct timeval {
time_t tv_sec; //seconds
suseconds_t tv_usec; //microseconds
};
参数说明
- which:指定定时器类型。有如下取值:
- ITIMER_REAL:以实际时间计算定时器的触发,触发 SIGALRM 信号。
- ITIMER_VIRTUAL:以该进程在用户态下花费的时间来计算,触发 SIGVTALRM 信号。
- ITIMER_PROF :以该进程在用户态下和内核态下所费的时间来计算,触发 SIGPROF 信号。
- new_value:一个 struct itimerval 结构体指针,用于设置定时器的第一次触发时间和之后每次触发的间隔时间。
- old_value:一个 struct itimerval 结构体指针,用于获取之前设置的定时器的值,如果不需要获取,则可以传递 NULL。
返回值
如果函数执行成功,返回值为0。如果函数执行失败,返回值为-1,并且可以通过 errno 变量来获取具体的错误信息。
示例:
1 #include<stdio.h>
2 #include<signal.h>
3 #include<unistd.h>
4 #include<sys/time.h>
5 #include<string.h>
6
7 void sigHandler(int sig)
8 {
9 static int i = 0;
10 printf("alarm signal, i = %d\n", i++);
11 }
12
13 int main(int argc, char** argv)
14 {
15 signal(SIGALRM, sigHandler);
16
17 struct itimerval timerval;
18 memset(&timerval, 0, sizeof(timerval));
19 timerval.it_interval.tv_usec = 500 * 1000; //500毫秒
20 timerval.it_value.tv_sec = 2;
21
22 int nRet = setitimer(ITIMER_REAL, &timerval, NULL);
23 if(nRet == -1)
24 {
25 perror("setitimer");
26 return -1;
27 }
28
29 while(1);
30 return 0;
31 }
abort()函数
abort()函数用于终止当前进程执行,当调用 abort() 函数时,会向当前进程发送 SIGABRT 信号,进程会异常终止。该函数定义如下:
#include <stdlib.h>
void abort(void);
注意事项:
- abort() 函数会直接导致进程异常终止,不会执行任何退出处理程序,包括 atexit() 注册的函数。
- abort() 函数退出时,会执行清理操作,包括刷新流缓冲区(调用 fflush(NULL) 函数)、关闭流(调用 fclose() 函数)等。
- 在调用 abort() 函数之前,建议在标准错误流中输出相应的错误信息,以便识别程序终止的原因。
- 当进程接收到 SIGABRT 信号时,通常会执行默认的信号处理函数,该函数会在标准错误流中打印信息,之后生成一个核心转储文件(core dump)。
可重入函数与不可重入函数
可重入函数:指的是一个函数可以被多个任务同时调用而不会产生错误或不确定的行为。可重入函数通常具有以下特点:
- 不使用全局变量、静态变量或其他共享资源。
- 不会修改传入的参数。
- 不会调用不可重入的函数。
- 不会使用不可重入的系统调用。
由于可重入函数的设计可以让多个任务同时安全地调用它,因此在并发环境中通常更可靠更安全。
不可重入函数:指的是一个函数在多个任务同时调用时可能会产生错误或不确定的结果。不可重入函数通常具有以下特点:
- 使用全局变量、静态变量或其他共享资源。
- 会修改传入的参数。
- 会调用不可重入的函数。
- 可能使用不可重入的系统调用。
不可重入函数在多任务环境中可能会导致线程安全问题,在并发编程中,尽量避免使用不可重入函数,以确保程序的可靠性和健壮性。
示例:
strtok() 就是一个不可重入函数,因为strtok内部维护了一个内部静态指针,保存上一次切割到的位置,如果信号的捕捉函数中也去调用strtok函数,则会造成切割字符串混乱。
1 #include<stdio.h>
2 #include<signal.h>
3 #include<string.h>
4 #include<unistd.h>
5
6 static char pBuf1[] = "1 2 3 4 5";
7 static char pBuf2[] = "a b c d e";
8
9 void sigHandler(int sig)
10 {
11 strtok(pBuf2, " ");
12 }
13
14 int main(int argc, char** argv)
15 {
16 signal(SIGINT, sigHandler);
17
18 char* pStr = strtok(pBuf1, " ");
19 while(pStr)
20 {
21 printf("%s\n", pStr);
22 sleep(1);
23 pStr = strtok(NULL, " ");
24 }
25
26 return 0;
27 }
输出:
应该使用 strtok_r 版本,r表示可重入,将所有 strtok() 函数替换为 strtok_r() 函数,上面问题即可解决。系统提供的不可重入函数,都会提供对应的可重入版本,在对应的 man 帮助中,一般是在末尾加上 _t。
信号方式回收子进程PCB
系统提供了名为 SIGCHLD 的信号,可以用来回收子进程。该信号产生条件如下:
- 子进程终止时。
- 子进程接收到 SIGSTOP 信号被挂起时。
- 子进程处在停止态,接受到 SIGCONT 信号后被唤醒时。
使用方法如下:
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>
#include<stdlib.h>
void signalHandler(int sig)
{
int status;
pid_t pid = waitpid(-1, &status, WUNTRACED | WCONTINUED);
printf("recv child process pid = %d\n", pid);
if(WIFEXITED(status))
printf("child process exited with %d\n", WEXITSTATUS(status));
else if(WIFSIGNALED(status))
printf("child process signaled with %d\n", WTERMSIG(status));
else if(WIFSTOPPED(status))
printf("child process stoped\n");
else if(WIFCONTINUED(status))
printf("child process continued\n");
}
int main(int argc, char** argv)
{
signal(SIGCHLD, signalHandler); //订阅 SIGCHLD 信号
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
return -1;
}
else if(pid == 0)
{
printf("child process pid = %d\n", getpid());
sleep(5);
exit(10);
}
else
{
printf("parent process pid = %d\n", getpid());
while(1);
}
return 0;
}