一、信号介绍
信号的概念
- 信号是信息的载体,是Linux/Unix环境下,古老而又经典的进程通信方式,现在依然是进程间通信的主要手段。
- 信号在我们的生活中随处可见,例如:
- 古代战争中的摔杯为号;
- 现代战争中的信号弹;
- 体育比赛中使用的信号枪;
- 信号的特点
- 简单
- 不能携带大量的信息
- 满足某个特定的条件下才会产生信号
二、信号的机制
进程A给进程B发送一个信号,进程B收到信号之前先执行自己的代码,收到进程A发送的信号后,不管此时代码执行到什么位置,都要停下来去处理进程A发来的信号,处理完后再执行自己的代码。与硬件的软中断相似----异步模式。但是信号是软件层面上实现的,通常被称为“软中断“。
每个进程收到的所有信号,都是由内核负责发送的。
进程A给进程B发送信号示意图:
1、信号的状态
-
信号有三种状态:产生,未决和递达
信号的产生状态
- 按键产生,如:Ctrl+c,Ctrl+z,Ctrl+\
- 系统调用时产生,如:kill,raise,abort
- 软件条件产生,如:定时器 alarm
- 硬件异常产生,如:非法访问内存(段错误),除0(浮点数除外),内存对齐出错(总线错误)
- 命令产生,如:kill命令
未决状态:产生和递达之间的状态。主要是由于阻塞(屏蔽)导致该状态
递达状态:递达并且到达另一个进程
2、信号的处理方式
- 执行默认处理动作
- 忽略信号(丢弃不处理)
- 捕捉信号(调用用户自定义的处理函数)
3、信号的特质
信号的实现的手段导致信号有很强的延时性,但是对于用户来说,时间非常短,不易发现。
Linux内核的进程控制块 PCB 是一个结构体,task_struct ,除了包含进程 id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号的相关信息,主要指的是阻塞信号集和未决信号集。
4、阻塞信号集和未决信号集
- 阻塞信号集保存的都是被当前进程阻塞的信号。如果当前进程收到的是阻塞信号集中的某些信号,这些信号需要暂时被阻塞,先不处理。
- 信号产生后由于某些原因(主要是阻塞)不能抵达,这类信号的集合称为未决信号集。在屏蔽接触器前,信号一直处于未决状态;如果信号从阻塞信号集中解除阻塞,则该信号会被处理,并从未决信号集中去除。
5、信号的四要素
- 通过 man 7 signal 可以查看信号相关信息
- 信号的编号
- 使用 kill -l 命令可以查看当前系统有哪些信号,不存编号为 0 的信号。其中 1 - 31 号信号被称为常规信号(也叫普通信号或者标准信号),34 - 64 被称为实时信号,驱动编程与硬件相关。
- 信号的名称
- 产生信号的事件
- 信号的默认处理动作
- Term:终止进程
- Ign:忽略信号(默认及时对该信号进行忽略动作)
- Core:终止进程,并生成 Core 文件。(查验死亡原因,用于 gdb 调试)
- Stop:停止(暂停)进程
- Cont:继续运行进程
-
需要注意的是 SIGKILL 信号和 SIGSTOP 信号不能被捕获(caught),阻塞(blocked)和忽略(ignored)。
-
几个常用的信号:
SIGINT、SIGQUIT、SIGKILL、SIGSEGV、SIGUSR1、SIGUSR2(这两个信号是操作系统交给用户自定义的两个信号),SIGPIPE、SIGALRM、SIGTERM、SIGCHID、SIGSTOP、SIGCONT
内核的信号实现机制
三、信号相关函数
1、signal 函数
- 函数作用:注册信号捕捉函数
- 函数原型:
- typedef void(*sighandler_t)(int); 回调函数
- sighandler_t signal(int signum, sighandler_t handler);
- 函数参数:
- signum:信号编号
- handler:信号处理回调函数
signal 函数
//signal函数测试---注册信号处理函数
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
//信号处理函数
void sighandler(int signo)
{
printf("signo==[%d]\n", signo);
}
int main()
{
//注册信号处理函数
signal(SIGINT, sighandler);//SIGINT 使用Ctrl + c触发该信号
//while(1)
{
sleep(10);
}
return 0;
}
2、kill 函数/命令
-
函数描述:给指定进程发送指定的信号
-
kill 命令:kill -SIGKILL 进程 PID
-
kill 函数原型:int kill(pid_t pid, int sig);
-
函数参数:
- sig:信号编号,不推荐直接使用数字,而是使用宏名,因为不同的操作系统系统编号可能不同,但是名称是一致的
- pid:
- pid > 0:发送信号 给 指定的进程
- pid = 0:发送信号 给 与当前调用 kill 函数的进程属于同一进程组的所有进程
- pid < -1:取pid的绝对值,发送信号给取绝对值后的 pid 对应的进程组
- pid = -1:发送信号 给 进程有权限发送的系统中的所有进程
-
函数返回值:
- 成功:返回 0
- 失败:返回 -1,并设置 errno
进程组:每个进程都属于一个进程组,进程组是一个或多个进程的集合,他们互相关联,共同完成一个实体任务,每个进程组都有一个进程组组长,默认进程组ID与进程组组长PID相同。
kill 函数测试
//signal函数测试---注册信号处理函数
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
//信号处理函数
void sighandler(int signo)
{
printf("signo==[%d]\n", signo);
}
int main()
{
//注册信号处理函数
signal(SIGINT, sighandler);
while(1)
{
sleep(1);
kill(getpid(), SIGINT);
}
return 0;
}
使用SIGUSR1和SIGUSR2在父子进程间交替数数
//使用SIGUSR1和SIGUSR2在父子进程间交替数数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
int num = 0;
int flag;
void func1(int signo)
{
printf("F:[%d]\n", num);
num += 2;
flag = 0;
sleep(1);
}
void func2(int signo)
{
printf("C:[%d]\n", num);
num += 2;
flag = 0;
sleep(1);
}
int main(int argc, char *argv[])
{
int ret;
pid_t pid;
pid = fork();
if(pid<0)
{
perror("fork error");
return -1;
}
else if(pid>0)
{
num=0;
flag = 1;
signal(SIGUSR1, func1);
while(1)
{
if(flag==0)
{
kill(pid, SIGUSR2);
flag = 1;
}
}
}
else if(pid==0)
{
num=1;
flag = 0;
signal(SIGUSR2, func2);
while(1)
{
if(flag==0)
{
kill(getppid(), SIGUSR1);
flag = 1;
}
}
}
}
3、abort 函数 raise 函数
raise函数
- 函数描述:给当前进程发送指定信号(自己发给自己)
- 函数原型:int raise(int sig);
- 函数返回值:
- 成功:返回 0
- 失败:返回 非 0 的值
- 函数拓展:raise(signo) == kill(getpid(), signo);
abort 函数
- 函数描述:给自己发送一个异常终止信号,(6)SIGABRT,并产生 Core 文件
- 函数原型:void abort(void);
- 函数拓展:abort() == kill(getpid(), SIGABRT);
raise函数 abort 函数测试
//raise和abort函数测-
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
//信号处理函数
void sighandler(int signo)
{
printf("signo==[%d]\n", signo);
}
int main()
{
//注册信号处理函数
signal(SIGINT, sighandler);
//给当前进程发送SIGINT信号
raise(SIGINT);
//给当前进程发送SIGABRT
abort();
while(1)
{
sleep(10);
}
return 0;
}
4、alarm 函数
-
函数描述:设置定时器(相当于闹钟)。在指定时间后,内核会给当前进程发送 14) SIGALRM 信号。进程收到该信号后,默认动作是终止。每个进程都有且仅有唯一的一个定时器。
-
函数原型:unsigned int alarm(unsigned int seconds);
-
函数参数:seconds:指定的时间
-
函数返回值:返回 0 或剩余的描述,不会失败。
例:
-
常用操作:取消定时器 alarm(0),返回的是旧闹钟剩余的秒数。
alarm 函数使用的是自然定时法。与进程的状态无关,就绪,运行,挂起,(阻塞、暂停)、终止、僵尸…无论进程处于哪种状态,alarm 都会正常计时。
练习:测试alarm 函数
//alarm 函数测试---定时器
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
//信号处理函数
void sighandler(int signo)
{
printf("signo==[%d]\n", signo);
}
int main()
{
//注册信号处理函数
signal(SIGINT, sighandler);
signal(SIGALRM, sighandler);
int n = alarm(5);
printf("first: n==[%d]\n", n);
sleep(2);
n = alarm(5);
//n = alarm(0); //取消时钟
printf("second: n==[%d]\n", n);
while(1)
{
sleep(10);
}
return 0;
}
编写一个程序,测试一秒能打印多少数字
//测试1秒钟可以数多少数字
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main()
{
alarm(1);
int i = 0;
while(1)
{
printf("[%d]", i++);
//注意去掉换行符能提高效率
}
return 0;
}
-
使用 time 命令可以查看当前程序执行的时间。程序运行的瓶颈在于IO操作,优化程序时优先考虑优化IO
-
实际执行时间 = 系统时间 + 用户时间 + 损耗时间
损耗的时间主要来自文件的IO操作,IO操作会有用户区到内核区的切换,切换的次数越多越耗时。
总结:
alarm:
1 每一个进程都只有一个时钟
2 alarm函数的返回值: 0 或者是上一个alarm剩余的秒数
3 alarm(0): 取消定时器
4 alarm函数发送的是SIGALRM信号
闹钟
实际执行时间 = 系统时间 + 用户时间 + 损耗时间
损耗时间= 实际执行时间-(系统时间 + 用户时间 )
每一个数字都直接打印:printf("[%d]\n", i++);
real 0m1.217s
user 0m0.120s
sys 0m0.252s
15734次
损耗时间= 1.217-(0.120+0.252)=0.845
文件重定向之后:
time ./alarm_uncle > test.log
real 0m1.003s
user 0m0.520s
sys 0m0.428s
2191879次
损耗时间=1.003-(0.520+0.428)=0.055
原因是: 调用printf函数打印数字遇到\n才会打印, 打印过程涉及到从
用户区到内核区的切换, 切换次数越多消耗的时间越长, 效率越低;
而使用文件重定向, 由于文件操作是带缓冲的, 所以涉及到用户区到内核区的
切换次数大大减少,从而使损耗降低.
5、setitimer函数
-
函数描述:设置定时器(闹钟)。可以替代 alarm 函数,精度是 微秒us 级别,可以实现周期定时。
-
函数原型:int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
-
函数参数:
-
which:指定定时方式
- 自然定时:ITIMER_REAL ---- 14)SIGALRM 计算自然时间
- 虚拟空间计时(用户空间):ITIMER_VIRTUAL ---- 26)SIGVTALRM 只计算进程占用 cpu 占用的时间
- 运行时计时(用户+内核):ITIMER_PROF ---- 27)SIGPROF 计算占用cpu 及执行系统调用的时间
-
new_value:struct itimerval,负责设定 timeout 时间
-
itimerval.it_value:设定第一次执行 function 所延迟的秒数,也就是过了多少秒开始计时。
-
itimerval.it_interval:设定以后每几秒执行 function
结构体:
struct itimerval { struct timerval it_interval;//闹钟触发周期 struct timerval it_value;//闹钟触发时间 }; struct timerval { long tv_sec; //秒 long tv_usec; //微秒 };
-
-
old_value:存放旧的 timerout 值,一般指定为 NULL
-
-
函数返回值:
- 成功:返回 0
- 失败:返回 -1,并设置 errno 值
-
功能:设置时钟, 能够周期性的触发时钟
四、信号集
1、未决信号集和阻塞信号集
阻塞信号集是当前进程要阻塞的信号的集合,未决信号集是当前进程中还还处于未决状态的信号的集合,这两个集合存储在内核的 PCB 中。
-
下面以 SIGINT 信号说明未决信号集和阻塞信号集之间的关系:
-
当进程收到一个 SIGINT 信号(信号编号为2),首先这个信号会保存在未决信号集中,此时对应的 2 号编号的这个位置上上被置为 1 ,表示2号信号也就是SIGINT信号当前为未决状态;这个信号需要被处理之前首先要在阻塞信号集中编号为2的位置检查该值是否为 1
- 如果为1,表示 SIGINT 信号被当前进程阻塞,这个信号就暂时不被处理,所以此时未决信号集上该位置的上的值仍然保持为1,表示该信号仍然处于未决状态
- 如果为0,表示 SIGINT 信号没有被当前进程阻塞,这个信号就需要被处理,内核会对 SIGINT 信号进行处理(执行默认动作,忽略或者执行用户自定义的信号处理函数),并且将未决信号集中编号为 2 的位置上将 1 变为 0,表示该信号已经处理了,这个时间非常短暂,用户感知不到。
当 SIGINT 信号从阻塞信号集中解除阻塞之后,该信号就会被处理。
-
2、信号集相关函数
由于信号集是属于内核的一块区域,用户并不能直接操作内核空间,为此,内核提供了一些信号集相关的接口函数,使用这些函数用户就可以完成对信号集的相关操作。
信号集是一个能表示多个信号的数据类型,sigset_set ,set 即表示一个信号集。既然是一个集合,那么必然就可以进行添加,删除等操作。
Tips:
上述变量类型的定义的查找有个小窍门:可以执行gcc的预处理命令:
gcc -E test.c -o test.i 这样头文件就会展开,可以直接到 test.i 文件中看到相关变量类型的定义。
-
信号集相关函数
-
int sigemptyset(sigset_t *set);
- 函数描述:将某个信号清 0
- 函数返回值:成功:0 失败:-1,并设置 errno值
-
int sigfillset(sigset_t *set);
- 函数描述:将某个信号置为1
- 函数返回值:成功:0 失败:-1,并设置 errno 值
-
int sigaddset(sigset_t *set, int signum);
- 函数描述:将某个信号加入信号集合中
- 函数返回值:成功:0 失败:-1,并设置 errno 值
-
int sigdelset(sigset_t *set,int signum);
- 函数描述:将某个信号从信号集合中删除
- 函数返回值:成功:0 失败:-1,并设置 errno 值
-
int sigismember(const sigset_t *set, int signum);
- 函数描述:判断某个信号是否在信号集中
- 函数返回值:
- 在:返回 1
- 不在:返回 0
- 出错:-1,并设置 errno 值
-
sigprocmask 函数
-
函数描述:用来屏蔽信号,解除屏蔽也可以使用该函数。本质就是读取或修改进程控制块 PCB 中的信号屏蔽字(阻塞信号集)。
注意:屏蔽信号只是将信号处理后延后执行(延至解除屏蔽);而忽略表示将信号丢弃处理。
-
函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
-
函数参数:
- how:假设当前的信号屏蔽字为 mask
- SIG_BLOCK:当 how 设置为此值,set 表示需要屏蔽的信号,相当于 mask = mask | set
- SIG_UNBLOCK:当 how 设置为此值,set 表示需要解除屏蔽的信号,相当于 mask = mask & ~set
- SIG_SETMASK:当 how 设置为此值,set 表示用于替代原始屏蔽集合的新屏蔽集合。相当于 mask = set,如果调用sigprocmask 解除了对当前某个信号的阻塞,则在 sigprocmask 返回前,至少将一个信号递达。
- set:传入参数,用户自定义的一个信号集合,根据 how 来指示如何修改当前信号屏蔽字
- oldset:传出参数,保存旧的信号屏蔽字
- how:假设当前的信号屏蔽字为 mask
-
函数返回值
- 成功:返回 0
- 失败:返回 -1 ,并设置 errno值
-
-
sigpending 函数
- 函数描述:读取当前进程的未决信号集
- 函数原型:int sigpending(sigset_t *set);
- 函数参数:set 传出参数
- 函数返回值:
- 成功:返回 0
- 失败:返回 -1,并设置 errno 值
信号集测试
编写程序,设置阻塞信号集并把所有常规信号的未决状态打印至屏幕
//信号集相关函数测试 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <signal.h> //信号处理函数 void sighandler(int signo) { printf("signo==[%d]\n", signo); } int main() { //注册SIGINT和SIGQUIT的信号处理函数 signal(SIGINT, sighandler); signal(SIGQUIT, sighandler); //定义sigset_t类型的变量 sigset_t pending, mask, oldmask; //初始化 sigemptyset(&pending); sigemptyset(&mask); sigemptyset(&oldmask); //将SIGINT和SIGQUIT加入到阻塞信号集中 sigaddset(&mask, SIGINT); sigaddset(&mask, SIGQUIT); //将mask中的SIGINT和SIGQUIT信号加入到阻塞信号集中 //sigprocmask(SIG_BLOCK, &mask, NULL); sigprocmask(SIG_BLOCK, &mask, &oldmask); int i = 1; int k = 1; while(1) { //获取未决信号集 sigpending(&pending); for(i=1; i<32; i++) { //判断某个信号是否在集合中 if(sigismember(&pending, i)==1) { printf("1"); } else { printf("0"); } } printf("\n"); if(k++%10==0) { //从阻塞信号集中解除对SIGINT和SIGQUIT的阻塞 //sigprocmask(SIG_UNBLOCK, &mask, NULL); sigprocmask(SIG_SETMASK, &oldmask, NULL); } else { sigprocmask(SIG_BLOCK, &mask, NULL); } sleep(1); } return 0; }
-
3、信号捕捉函数
-
signal 函数
-
sigaction 函数
- 函数描述:注册一个信号处理函数
- 函数原型:int sigaction(int signum, const strcut sigaction *act, struct sigaction *oldact);
- 函数参数:
- signum:捕捉的信号
- act:传入参数,新的处理方式
- oldact:传出参数,旧的处理方式
struct sigaciotn { void (*sa_handler)(int); // 信号处理函数 void (*sa_sigaction)(int,siginfo_t*,void*); // 信号处理函数 sigset_t sa_mask; // 信号处理函数执行期间需要阻塞的信号 int sa_flags; // 通常为0,表示使用默认标识 void (*sa_restorer)(void); };
sigaction结构体总结:
- sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为 SIG_IGN 表示忽略 或 SIG_DFL 表示执行默认处理动作
- sa_mask:用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行的期间这个信号不会再度发生。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
- sa_flags:通常设置为 0,使用默认属性
- sa_restorer:已不再使用
练习:编写程序,使用 sigaction 函数注册信号处理函数,并使用这个程序验证信号是否支持排队
//sigaction函数测试---注册信号处理函数
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
//信号处理函数
void sighandler(int signo)
{
printf("signo==[%d]\n", signo);
sleep(4);
}
int main()
{
//注册信号处理函数
struct sigaction act;
act.sa_handler = sighandler;
sigemptyset(&act.sa_mask); //在信号处理函数执行期间, 不阻塞任何信号
sigaddset(&act.sa_mask, SIGQUIT);
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
signal(SIGQUIT, sighandler);
while(1)
{
sleep(10);
}
return 0;
}
-
知识点:信号处理不支持排队
- 在 XXX 信号函数处理执行期间,XXX 信号是被阻塞的,如果该信号产生了多次,在 XXX 信号处理函数结束之后,该 XXX 信号只被处理一次
- 在 XXX 信号函数处理执行期间,如果阻塞了 YYY 信号,若 YYY 信号产生了多次,当 XXX 信号处理函数结束之后,YYY 信号只被处理一次
-
内核实现信号捕捉的过程
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
- 用户注册了 SIGQUIT 信号的处理函数 sighandler
- 当前正在执行 main 函数,这时发生中断或异常切换到内核态
- 在中断处理完之后要返回用户态的 main 函数之前检查到了有信号 SIGQUIT 递达
- 内核决定返回用户态后不是恢复 main 函数的上下文进行继续执行,而是执行 sighandler 函数,sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被用的关系,是两个独立的控制流程
- sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态
- 如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。
-
未决信号集和阻塞信号集的关系1
-
未决信号集和阻塞信号集的关系2
五、SIGCHLD信号
1、产生SIGCHLD信号的条件
- 子进程结束的时候
- 子进程收到 SIGSTOP 信号
- 当子进程停止时,收到 SIGCONT 信号
2、SIGCHLD信号的作用
-
子进程退出后,内核会给它的父进程发送 SIGCHLD 信号,父进程收到这个信号后可以对子进程进行回收
-
使用 SIGCHLD 信号完成对子进程的回收可以避免父进程阻塞等待而不能执行其他操作,只有当父进程收到 SIGCHLD 信号之后才去调用信号捕捉函数完成对子进程的回收,未收到 SIGCHLD 信号之前可以处理其他操作。
3、使用SIGCHLD信号完成对子进程的回收
-
练习:父进程创建3个子进程,然后让父进程捕获 SIGCHLD 信号完成对子进程的回收
注意:
1、有可能还未完成信号处理函数的注册,三个子进程都退出了
解决方法:可以在 fork 之前先将 SIGCHLD 信号阻塞,当完成信号处理函数的注册后再解除阻塞。
2、当 SIGCHLD 信号函数处理期间,SIGCHLD 信号如果再次产生是被阻塞的,而且产生了多次,则该信号只会被处理一次,这样可能就会产生僵尸进程。
解决方法:可以在信号处理函数里使用 while(1) 循环回收,这样就有可能出现捕获一次 SIGCHLD 信号但是回收了多个子进程的情况,从而避免产生僵尸进程。
//父进程使用SICCHLD信号完成对子进程的回收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void waitchild(int signo)
{
pid_t wpid;
//回收子进程
while(1)
{
wpid = waitpid(-1, NULL, WNOHANG);
if(wpid>0)
{
printf("child is quit, wpid==[%d]\n", wpid);
}
else if(wpid==0)
{
printf("child is living, wpid==[%d]\n", wpid);
break;
}
else if(wpid==-1)
{
printf("no child is living, wpid==[%d]\n", wpid);
break;
}
}
}
int main()
{
int i = 0;
int n = 3;
//将SIGCHLD信号阻塞
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, NULL);
for(i=0; i<n; i++)
{
//fork子进程
pid_t pid = fork();
if(pid<0) //fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid>0) //父进程
{
printf("father: fpid==[%d], cpid==[%d]\n", getpid(), pid);
sleep(1);
}
else if(pid==0) //子进程
{
printf("child: fpid==[%d], cpid==[%d]\n", getppid(), getpid());
break;
}
}
//父进程
if(i==3)
{
printf("[%d]:father: fpid==[%d]\n", i, getpid());
//signal(SIGCHLD, waitchild);
//注册信号处理函数
struct sigaction act;
act.sa_handler = waitchild;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sleep(5);
sigaction(SIGCHLD, &act, NULL);
//解除对SIGCHLD信号的阻塞
sigprocmask(SIG_UNBLOCK, &mask, NULL);
while(1)
{
sleep(1);
}
}
//第1个子进程
if(i==0)
{
printf("[%d]:child: cpid==[%d]\n", i, getpid());
//sleep(1);
}
//第2个子进程
if(i==1)
{
printf("[%d]:child: cpid==[%d]\n", i, getpid());
sleep(1);
}
//第3个子进程
if(i==2)
{
printf("[%d]:child: cpid==[%d]\n", i, getpid());
sleep(1);
}
return 0;
}