目录
1. 进程基础知识
2. 查看进程
3. 杀死进程
4. 获取进程标识符
5. 进程创建
6. 进程终止
7. 进程等待
8. 进程程序替换
9. 进程间通信之管道
9.1 匿名管道
9.2 命名管道(FIFO)
10. 进程间通信之共享内存
11. 进程间通信之信号
11.1 Linux信号
11.2 产生信号
11.3 捕捉信号
11.4 信号集
11.4.1 未决信号集和阻塞信号集
11.4.2 信号集函数
1. 进程基础知识
详见
5.1 进程、线程基础知识 | 小林codinghttps://xiaolincoding.com/os/4_process/process_base.html#%E8%BF%9B%E7%A8%8B
2. 查看进程
- ps 命令
ps aux / ajx
# a:显示终端上的所有进程,包括其他用户的进程
# u:显示进程的详细信息
# x:显示没有控制终端的进程
# j:列出与作业控制相关的进程
STAT(进程状态) | 描述 |
---|---|
D | 不可中断 Uninterruptible usually IO |
R | 正在运行,或在队列中的进程 |
S | 处于休眠状态 |
T | 停止或被追踪 |
Z | 僵尸进程 |
W | 进入内存交换(从内核 2.6 开始无效) |
X | 死掉的进程 |
< | 高优先级 |
N | 低优先级 |
s | 包含子进程 |
+ | 位于前台的进程组 |
- /proc系统文件夹
- top命令(实时显示进程动态)
top # 默认3s刷新一次
top -d 5 # 5s刷新一次
top -d 8 # 8s刷新一次
在top命令执行后,可以按以下按键对显示的结果进行排序:
- M 根据内存使用量排序
- P 根据CPU占有率排序
- T 根据进程运行时间长短排序
- U 根据用户名来筛选进程
- K 输入指定的PID杀死进程
3. 杀死进程
kill PID # 默认信号是15,等价于kill -15 PID,或kill -SIGTERM PID
kill -9 PID # 强制杀死,等价于kill -SIGKILL PID
killall name # 根据进程名杀死进程
kill -l # 列出所有信号
4. 获取进程标识符
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); // 获取进程ID
pid_t getppid(void); // 获取父进程ID
5. 进程创建
#include <unistd.h>
pid_t fork(void);
// 父进程返回子进程ID,子进程返回0;失败时返回-1并设置errno
fork函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和原进程相同,比如堆指针、栈指针和标志寄存器的值。但也有许多属性被赋予了新的值,比如该进程的PPID被设置成原进程的PID,信号位图被清除(原进程设置的信号处理函数不再对新进程起作用)。
读时共享,写时拷贝:
子进程的代码与父进程完全相同,同时它还会复制父进程的数据(堆数据、栈数据和静态数据)。数据的复制采用的是所谓的写时复制(copy on write),即只有在任一进程(父进程或子进程)对数据执行了写操作时,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据)。即便如此,如果我们在程序中分配了大量内存,那么使用fork时也应当十分谨慎,尽量避免没必要的内存分配和数据复制。
此外,创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1。不仅如此,父进程的用户根目录、当前工作目录等变量的引用计数均会加1。
#include <unistd.h>
#include <stdio.h>
int gval = 10;
int main()
{
int lval = 20;
gval++;
lval += 5;
pid_t pid = fork();
if (pid == 0) // 子进程
{
gval += 2;
lval += 2;
}
else // 父进程
{
gval -= 2;
lval -= 2;
}
if (pid == 0) // 子进程
{
printf("子进程gval %2d lval %2d\n", gval, lval);
}
else // 父进程
{
printf("父进程gval %2d lval %2d\n", gval, lval);
}
return 0;
}
// 父进程gval 9 lval 23
// 子进程gval 13 lval 27
6. 进程终止
- C/C++库函数exit
- 系统调用函数_exit
- main函数内部return语句(main函数内部,return n等价于exit(n))
- Ctrl+C,信号终止
#include <stdlib.h>
void exit(int status);
#include <unistd.h>
void _exit(int status);
// status 进程退出状态码,0表示正常退出,非0表示异常退出
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("hello");
exit(0);
return 0;
}
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello");
_exit(0);
return 0;
}
7. 进程等待
父进程运行结束,但子进程还在运行,这样的子进程就称为孤儿进程。孤儿进程将被init进程(PID为1)所收养,并由init进程对它们完成状态收集工作。因此,孤儿进程并不会有什么危害。
子进程运行结束,而父进程又没有回收子进程、释放子进程占用的资源,此时子进程将成为一个僵尸进程。僵尸进程不能被kill -9杀死。
僵尸进程的危害:
- 占用系统资源,造成资源浪费。
- 占用PID,如果存在大量的僵尸进程,将因为没有可用的PID而导致系统不能产生新的进程。
父进程应该等待子进程运行结束,回收子进程资源,获取子进程退出状态信息,才能避免僵尸进程。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status);
// wait函数将阻塞进程,直到该进程的某个子进程运行结束
// 成功时返回运行结束的子进程ID,失败时返回-1
// status 输出型参数,获取子进程退出状态信息,不关心则设置为NULL
// 退出状态信息相关宏函数:
// WIFEXITED(status) 非0,进程正常终止
// WEXITSTATUS(status) 如果上宏为真,获取进程退出状态码
// WIFSIGNALED(status) 非0,进程异常终止
// WTERMSIG(status) 如果上宏为真,获取使进程终止的信号编号
// WIFSTOPPED(status) 非0,进程处于暂停状态
// WSTOPSIG(status) 如果上宏为真,获取使进程暂停的信号的编号
// WIFCONTINUED(status) 非0,进程暂停后已经继续运行
pid_t waitpid(pid_t pid, int* status, int options);
// waitpid只等待由pid参数指定的子进程
// 成功时返回运行结束的子进程ID,失败时返回-1
// pid 目标子进程ID,如果值为-1,则和wait函数相同,即等待任意一个子进程结束
// status 和wait函数相同
// options 可以控制waitpid函数的行为,通常为WNOHANG,此时waitpid调用是非阻塞的
// 如果目标子进程还没有结束或意外终止,则waitpid立即返回0
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int status;
pid_t pid = fork(); // 创建子进程1
if (pid == 0) // 子进程1
{
return 3; // 子进程1终止
}
else // 父进程
{
printf("子进程1的PID %d\n", pid);
pid = fork(); // 创建子进程2
if (pid == 0) // 子进程2
{
exit(7); // 子进程2终止
}
else // 父进程
{
printf("子进程2的PID %d\n", pid);
wait(&status); // 回收子进程资源,获取子进程退出状态信息
if (WIFEXITED(status)) // 判断子进程是否正常终止
{
printf("子进程退出状态码 %d\n", WEXITSTATUS(status));
}
wait(&status); // 因为之前创建了2个进程,所以再次调用wait函数和宏
if (WIFEXITED(status))
{
printf("子进程退出状态码 %d\n", WEXITSTATUS(status));
}
sleep(30); // 为暂停父进程终止而插入的代码,此时可以查看子进程的状态
}
}
return 0;
}
// 子进程1的PID 1721
// 子进程2的PID 1722
// 子进程退出状态码 7
// 子进程退出状态码 3
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int status;
pid_t pid = fork(); // 创建子进程
if (pid == 0) // 子进程
{
sleep(15); // 子进程延迟15s
return 24;
}
else // 父进程
{
while (!waitpid(-1, &status, WNOHANG)) // 这个waitpid调用是非阻塞的
{
sleep(3);
puts("sleep 3sec.");
}
if (WIFEXITED(status))
{
printf("子进程退出状态码 %d\n", WEXITSTATUS(status));
}
}
return 0;
}
// sleep 3sec.
// sleep 3sec.
// sleep 3sec.
// sleep 3sec.
// sleep 3sec.
// 子进程退出状态码 24
8. 进程程序替换
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的ID并未改变。
exec函数族:
#include <unistd.h>
// 标准C库中的函数
int execl(const char* path, const char* arg, ...);
int execlp(const char* file, const char* arg, ...);
int execle(const char* path, const char* arg, ..., char* const envp[]);
int execv(const char* path, char* const argv[]);
int execvp(const char* file, char* const argv[]);
int execvpe(const char* file, char* const argv[], char* const envp[]);
// Linux系统调用函数
int execve(const char* filename, char* const argv[], char* const envp[]);
// 成功时不返回,原程序中exec调用之后的代码都不会执行(被替换了);失败时返回-1并设置errno
// 参数:
// path 可执行文件路径,绝对路径、相对路径均可
// file 可执行文件名称,该文件的具体位置在环境变量PATH中搜索
// arg 可执行文件的命令行参数列表,以空指针结尾
// argv 可执行文件的命令行参数数组
// arg和argv都会被传递给可执行文件的main函数
// envp 新进程的环境变量
// 命名:
// l(list) 命令行参数列表
// v(vector) 命令行参数数组
// p(path) 在环境变量PATH中搜索file
// e(environment) 使用新的环境变量
9. 进程间通信之管道
进程间通信(Inter Process Communication,IPC)基础知识详见
5.2 进程间有哪些通信方式? | 小林codinghttps://xiaolincoding.com/os/4_process/process_commu.html
9.1 匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);
// 成功时返回0,失败时返回-1并设置errno
// pipefd 输出型参数,pipefd[0]保存管道读端文件描述符,pipefd[1]保存管道写端文件描述符
管道默认是阻塞的:如果管道中没有数据,read阻塞;如果管道满了,write阻塞。
如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),有进程向管道的写端写数据,那么该进程会收到一个信号SIGPIPE,通常会导致进程异常终止。
9.2 命名管道(FIFO)
创建:
- mkfifo命令
mkfifo 管道的文件名
- mkfifo函数
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char* pathname, mode_t mode);
// 成功时返回0,失败时返回-1并设置errno
// pathname FIFO的文件名
// mode 文件权限
一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于FIFO。
10. 进程间通信之共享内存
共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种IPC机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
共享内存是最高效的IPC机制,因为它不涉及进程之间的任何数据传输。这种高效率带来的问题是,我们必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件。因此,共享内存通常和其他进程间通信方式一起使用。
管理共享内存信息的结构体:
struct shmid_ds
{
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
Linux共享内存的API都定义在sys/shm.h头文件中,包括4个系统调用:shmget、shmat、shmdt和shmctl。
shmget
shmget系统调用创建一段新的共享内存,或者获取一段已经存在的共享内存。其定义如下:
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
// 成功时返回共享内存的标识符,失败时返回-1并设置errno
// key 标识共享内存段,key_t是一个整型
// size 共享内存的大小
// 如果是创建新的共享内存,则size值必须被指定;如果是获取已经存在的共享内存,则可以把size设置为0
// shmflg 权限(如0666)和附加属性
// 附加属性:IPC_CREAT:创建共享内存
// IPC_EXCL:判断共享内存是否存在,需要和IPC_CREAT一起使用
// 示例:IPC_CREAT | IPC_EXCL | 0666
如果shmget用于创建共享内存,则这段共享内存的所有字节都被初始化为0,与之关联的内核数据结构shmid_ds将被创建并初始化。
shmat和shmdt
共享内存被创建/获取之后,我们不能立即访问它,而是需要先将它关联到进程的地址空间中。使用完共享内存之后,我们也需要将它从进程地址空间中分离。这两项任务分别由如下两个系统调用实现:
#include <sys/shm.h>
void* shmat(int shmid, const void* shmaddr, int shmflg);
// 成功时返回共享内存的首地址,失败时返回(void*)-1并设置errno
// shmid 共享内存的标识符,即shmget的返回值
// shmaddr 指定将共享内存关联到进程的哪块地址空间,通常为NULL,让内核指定
// shmflg 对共享内存的操作:SHM_RDONLY:只读模式
// 0:读写模式
int shmdt(const void* shmaddr);
// 成功时返回0,失败时返回-1并设置errno
// shmaddr 共享内存的首地址
shmctl
shmctl系统调用控制共享内存的某些属性。其定义如下:
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
// 成功时返回0,失败时返回-1并设置errno
// shmid 共享内存的标识符,即shmget的返回值
// cmd 对共享内存的操作
// IPC_STAT:获取共享内存的状态,把共享内存的shmid_ds结构复制到buf中
// IPC_SET :设置共享内存的状态,把buf的uid、gid、mode复制到共享内存的shmid_ds结构中
// IPC_RMID:删除共享内存段
// buf 指向管理共享内存信息的结构体
11. 进程间通信之信号
11.1 Linux信号
前31个信号是常规信号,其余为实时信号。关于信号的详细信息可以用man 7 signal查看。
信号的默认动作:
编号 | 宏值 | 默认动作 | 含义 |
---|---|---|---|
1 | SIGHUP | Term | 控制终端挂起 |
2 | SIGINT | Term | 键盘输入以中断进程(Ctrl+C) |
3 | SIGQUIT | Core | 键盘输入使进程退出(Ctrl+\) |
4 | SIGILL | Core | 非法指令 |
5 | SIGTRAP | Core | 断点陷阱,用于调试 |
6 | SIGABRT | Core | 进程调用abort函数时生成该信号 |
7 | SIGBUS | Core | 总线错误,错误内存访间 |
8 | SIGFPE | Core | 浮点异常 |
9 | SIGKILL | Term | 终止一个进程。该信号不可被捕获或者忽略 |
10 | SIGUSR1 | Term | 用户自定义信号之一 |
11 | SIGSEGV | Core | 非法内存段引用 |
12 | SIGUSR2 | Term | 用户自定义信号之二 |
13 | SIGPIPE | Term | 往读端被关闭的管道或者socket连接中写数据 |
14 | SIGALRM | Term | 由alarm或setitimer设置的实时闹钟超时引起 |
15 | SIGTERM | Term | 终止进程。kill命令默认发送的信号就是SIGTERM |
16 | SIGSTKFLT | Term | 早期的Linux使用该信号来报告数学协处理器栈错误 |
17 | SIGCHLD | Ign | 子进程状态发生变化(退出或者暂停) |
18 | SIGCONT | Cont | 启动被暂停的进程(Ctrl+Q)。如果目标进程未处于暂停状态,则信号被忽略 |
19 | SlGSTOP | Stop | 暂停进程(Ctrl+S)。该信号不可被捕获或者忽略 |
20 | SIGTSTP | Stop | 挂起进程(Ctrl+Z) |
21 | SIGTTIN | Stop | 后台进程试图从终端读取输入 |
22 | SIGTTOU | Stop | 后台进程试图往终端输出内容 |
23 | SIGURG | Ign | socket连接上接收到紧急数据 |
24 | SIGXCPU | Core | 进程的CPU使用时间超过其软限制 |
25 | SIGXFSZ | Core | 文件尺寸超过其软限制 |
26 | SIGVTALRM | Term | 与SIGALRM类似,不过它只统计本进程用户空间代码的运行时间 |
27 | SIGPROF | Term | 与SIGALRM类似,它同时统计用户代码和内核的运行时间 |
28 | SIGWINCH | Ign | 终端窗口大小发生变化 |
29 | SIGIO | Term | IO就绪,比如socket上发生可读、可写事件。因为TCP服务器可触发SIGIO的条件很多,故而SIGIO无法在TCP服务器中使用。SIGIO信号可用在UDP服务器中,不过也非常少见 |
30 | SIGPWR | Term | 对于使用UPS(Uninterruptable Power Supply)的系统,当电池电量过低时,SIGPWR信号将被触发 |
31 | SIGSYS | Core | 非法系统调用 |
11.2 产生信号
- 终端按键,如Ctrl+C产生SIGINT(2)信号,Ctrl+Z产生SIGTSTP(20)信号。
- 硬件异常,如除0操作产生SIGFPE(8)信号,非法访问内存产生SIGSEGV(11)信号。
- 软件异常,如往读端被关闭的管道或者socket连接中写数据产生SIGPIPE(13)信号,alarm或setitimer设置的定时器时间到产生SIGALRM(14)信号。
- 系统调用,如kill函数给任何进程或进程组发送信号,raise函数给当前进程发送信号,abort函数给当前进程发送SIGABRT(6)信号。
- kill命令,格式:kill [-信号的编号或宏值] PID,本质是调用kill函数实现的。
kill、raise、abort
#include <signal.h>
int kill(pid_t pid, int sig);
// 给任何进程或进程组发送信号
// 成功时返回0,失败时返回-1并设置errno
// pid>0 信号发送给PID为pid的进程
// pid=0 信号发送给本进程组内的其他进程
// pid=-1 信号发送给除init进程外的所有进程,但发送者需要拥有对目标进程发送信号的权限
// pid<-1 信号发送给组ID为-pid的进程组中的所有成员
// sig 要发送的信号的编号或宏值,0表示不发送任何信号
#include <signal.h>
int raise(int sig);
// 给当前进程发送信号
// 成功时返回0,失败时返回非0
#include <stdlib.h>
void abort(void);
// 给当前进程发送SIGABRT信号,异常终止该进程
alarm、setitimer
#include <unistd.h>
unsigned alarm(unsigned seconds);
// 设置定时器(闹钟),seconds秒之后向当前进程发送SIGALAR信号,该信号的默认动作是结束进程
// 之前没有定时器,返回0;之前有定时器,返回之前的定时器剩余的时间
#include <sys/time.h>
int setitimer(int which, const struct itimerval* restrict value,
struct itimerval* restrict ovalue);
// 设置周期性定时器(闹钟),将which指定的定时器设置为value指向的结构体中指定的值,精度微秒
// 成功时返回0,失败时返回-1并设置errno
// which 定时器类型
// ITIMER_REAL :以真实时间来计算,时间到发送SIGALRM
// ITIMER_VIRTUAL:以该进程在用户态下所消耗的时间来计算,时间到发送SIGVTALRM
// ITIMER_PROF :以该进程在用户态和内核态下所消耗的时间来计算,时间到发送SIGPROF
// value 新的定时器
// ovalue 旧的定时器,一般不使用,通常为NULL
struct itimerval // 管理定时器的结构体
{
struct timeval it_interval; // 时间间隔
struct timeval it_value; // 计时时长
};
// 第一次计时it_value时长发送信号,再往后的信号每隔一个it_interval发送一次
struct timeval // 管理时间的结构体
{
time_t tv_sec; // 秒数
suseconds_t tv_usec; // 微秒
};
11.3 捕捉信号
signal
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// man 2 signal查询,手册说应该避免使用它,请使用sigaction
// 成功时返回前一次调用signal函数时传入的函数指针
// 如果是第一次调用signal,返回信号signum对应的默认处理函数指针SIG_DEF
// 失败时返回SIG_ERR并设置errno
// signum 要捕捉的信号的编号或宏值,除SIGKILL(9)和SIGSTOP(19)
// handler 捕捉到的信号如何处理
// SIG_IGN:忽略信号
// SIG_DFL:使用信号默认动作
// 自定义信号处理函数,是回调函数
sigaction
#include <signal.h>
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);
// 成功时返回0,失败时返回-1并设置errno
// signum 要捕捉的信号的编号或宏值,除SIGKILL(9)和SIGSTOP(19)
// act 捕捉到的信号如何处理
// oldact 上一次的处理动作,一般不使用,通常为NULL
struct sigaction
{
void (*sa_handler)(int); // SIG_IGN或SIG_DFL或自定义信号处理函数(回调函数)
void (*sa_sigaction)(int, siginfo_t *, void *); // 不常用
sigset_t sa_mask; // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号
int sa_flags; // 通常为0,表示使用默认属性(即使用sa_handler)
void (*sa_restorer)(void); // 已弃用
};
11.4 信号集
11.4.1 未决信号集和阻塞信号集
信号递达、未决、阻塞的概念:
- 实际执行信号的处理动作称为信号递达(Delivery)。
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞(Block)某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
图片来源:程序员成长之旅——进程信号_sigaction-CSDN博客
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
/* A `sigset_t' has a bit for each signal. */
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
11.4.2 信号集函数
#include <signal.h>
int sigemptyset(sigset_t* set);
// 清空信号集(将信号集中所有标志位置为0)
// 成功时返回0, 失败时返回-1
int sigfillset(sigset_t* set);
// 设置所有信号(将信号集中所有标志位置为1)
// 成功时返回0, 失败时返回-1
int sigaddset(sigset_t* set, int signum);
// 将信号signum添加到信号集中(对应的标志位置为1)
// 成功时返回0, 失败时返回-1
int sigdelset(sigset_t* set, int signum);
// 将信号signum从信号集中删除(对应的标志位置为0)
// 成功时返回0, 失败时返回-1
int sigismember(const sigset_t* set, int signum);
// 判断signum是否在信号集中
// 在返回1,不在返回0,失败时返回-1
#include <signal.h>
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
// 将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
// 成功时返回0, 失败时返回-1并设置errno
// how 如何对内核阻塞信号集进行处理
// SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
// 假设内核中默认的阻塞信号集是mask, mask | set
// SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
// mask &= ~set
// SIG_SETMASK:覆盖内核中原来的值
// set 已经初始化好的用户自定义的信号集
// oldset 保存设置之前的内核中的阻塞信号集的状态,可以是NULL
int sigpending(sigset_t* set);
// 获取内核中的未决信号集
// 成功时返回0, 失败时返回-1并设置errno