目录
1. 进程基础知识
详见
5.1 进程、线程基础知识 | 小林codinghttps://xiaolincoding.com/os/4_process/process_base.html#%E8%BF%9B%E7%A8%8B
2. 查看进程
2.1 ps命令
ps aux / ajx
# a 显示终端上的所有进程,包括其他用户的进程
# u 面向用户格式
# j 作业控制格式
# x 显示没有控制终端的进程
第一行表示进程的属性,就像表头一样,其中STAT表示进程状态:
运行一个死循环的程序,并查看该进程:
1. 运行程序
2. 为了方便操作和观察,复制SSH渠道并分屏:
3. 查看进程
2.2 /proc系统文件夹
ls /proc/pid # 查看PID为pid的进程信息
2.3 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. 进程地址空间
每个进程都有一套独立的虚拟地址空间,进程持有的虚拟地址会通过CPU芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存。
虚拟地址与物理地址之间通过页表来映射,如下图:
6. 进程创建
#include <unistd.h>
pid_t fork(void);
// 父进程返回子进程ID,子进程返回0;失败时返回-1并设置errno
读时共享,写时拷贝:
子进程的代码与父进程完全相同,同时它还会拷贝父进程的数据。数据的拷贝采用写时拷贝(copy on writte)技术,即只有在任一进程(父进程或子进程)对数据执行了写操作时,拷贝才会发生。
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h> // pid_t在sys/types.h头文件中
int gval = 10;
int main()
{
int lval = 20;
gval++;
lval += 5;
pid_t pid = fork();
if (pid == 0) // 子进程
{
gval += 2;
lval += 2;
}
else if (pid > 0) // 父进程
{
gval -= 2;
lval -= 2;
}
if (pid == 0) // 子进程
{
printf("子进程 gval: %2d, lval: %2d\n", gval, lval);
}
else if (pid > 0) // 父进程
{
printf("父进程 gval: %2d, lval: %2d\n", gval, lval);
}
return 0;
}
// 父进程 gval: 9, lval: 23
// 子进程 gval: 13, lval: 27
7. 进程终止
正常终止(可以通过echo $?查看进程退出码):
- 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 进程退出码
C标准库函数exit和系统调用接口_exit的区别:
#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;
}
8. 进程等待
父进程运行结束,但子进程还在运行,这样的子进程就称为孤儿进程。孤儿进程将被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
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
status是位图:
退出码和信号不同时存在。
位运算获取退出码:(status >> 8) & 0xFF (二进制11111111=十六进制FF)
位运算获取信号: status & 0x7F (二进制01111111=十六进制7F)
也可通过退出状态信息相关的宏函数获取退出码和信号:
WIFEXITED(status) // 非0,进程正常终止
WEXITSTATUS(status) // 如果上宏为真,获取进程退出码
WIFSIGNALED(status) // 非0,进程异常终止
WTERMSIG(status) // 如果上宏为真,获取使进程终止的信号编号
WIFSTOPPED(status) // 非0,进程处于暂停状态
WSTOPSIG(status) // 如果上宏为真,获取使进程暂停的信号编号
WIFCONTINUED(status) // 非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 if (pid > 0) // 父进程
{
printf("子进程1的PID: %d\n", pid);
pid = fork(); // 创建子进程2
if (pid == 0) // 子进程2
{
exit(7); // 子进程2终止
}
else if (pid > 0) // 父进程
{
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: 11086
// 子进程2的PID: 11087
// 子进程退出码: 3
// 子进程退出码: 7
#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 if (pid > 0) // 父进程
{
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
9. 进程程序替换
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的ID并未改变。
exec函数族:
#include <unistd.h>
// 系统调用接口
int execve(const char* filename, char* const argv[], char* const envp[]);
// 标准C库函数(底层都调用execve)
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[]);
// 成功时不返回,原程序中exec调用之后的代码都不会执行(被替换了);失败时返回-1并设置errno
// 参数:
// path 可执行文件路径,绝对路径、相对路径均可
// file 可执行文件名称,该文件的具体位置在环境变量PATH中搜索
// arg 可执行文件的命令行参数列表
// argv 可执行文件的命令行参数数组
// arg和argv都会被传递给可执行文件的main函数
// envp 新进程的环境变量
// 命名:
// l(list) 命令行参数列表
// v(vector) 命令行参数数组
// p(path) 在环境变量PATH中搜索file
// e(environment) 自定义环境变量
使用方法示例(以在进程内部执行ls命令为例):
// 命令行参数数组
char* argv[] = {"ls", "-a", "-l", NULL};
// 自定义环境变量,是覆盖式传入,想要添加或修改环境变量用putenv函数
char* envp[] = {"PATH=/usr/local/bin:/usr/bin", NULL};
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
execv("/usr/bin/ls", argv);
execlp("ls", "ls", "-a", "-l", NULL);
execvp("ls", argv);
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, envp);
execvpe("ls", argv, envp);
10. 进程间通信之管道
进程间通信(Inter Process Communication),简称IPC。
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的。
管道和信号依赖于内核,但共享内存无需内核介入。
10.1 匿名管道
管道符 |:将管道符左边命令的输出结果,作为右边命令的输入。
这种管道是没有名字的,称为匿名管道,用完了就销毁。
匿名管道的创建,需要通过下面这个系统调用:
#include <unistd.h>
int pipe(int pipefd[2]);
// 成功时返回0,失败时返回-1并设置errno
// pipefd 输出型参数,pipefd[0]保存管道读端文件描述符,pipefd[1]保存管道写端文件描述符
// 记忆:0像一张嘴,是读端,1像一支笔,是写段
管道的本质是内核中的一个缓冲区,缓冲区的存储能力是有限的。
管道默认是阻塞的:如果管道中没有数据,read阻塞;如果管道满了,write阻塞。
如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),有进程向管道的写端写数据,那么该进程会收到一个信号SIGPIPE,通常会导致进程异常终止。
如何用管道实现进程间通信?
我们可以使用fork创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个fd[0]与fd[1],两个进程就可以通过各自的fd写入和读取同一个管道文件实现跨进程通信了。
管道只能一端写入,另一端读出,所以上面这种模式容易造成混乱,因为父进程和子进程都可以同时写入,也都可以读出。如果仅需要父进程给子进程通信,通常的做法是:
- 父进程关闭读取的fd[0],只保留写入的fd[1];
- 子进程关闭写入的fd[1],只保留读取的fd[0];
所以说如果需要双向通信,则应该创建两个管道。
到这里,我们仅仅解析了使用管道进行父进程与子进程之间的通信,但是在我们shell里面并不是这样的。
在shell里面执行A | B命令的时候,A进程和B进程都是shell创建出来的子进程,A和B之间不存在父子关系,它俩的父进程都是shell。
所以说,在shell里通过“|”匿名管道将多个命令连接在一起,实际上也就是创建了多个子进程,那么在我们编写shell脚本时,能使用一个管道搞定的事情,就不要多用一个管道,这样可以减少创建子进程的系统开销。
子进程发送数据给父进程,父进程读取到数据,然后打印:
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
{
perror("pipe");
exit(1);
}
pid_t pid = fork();
if (pid == 0) // 子进程
{
printf("我是子进程,我的PID为%d\n", getpid());
close(pipefd[0]); // 关闭子进程的读端
// 子进程每隔2s不断写入数据
while (1)
{
char* msg = "这是子进程给父进程发送的数据";
write(pipefd[1], msg, strlen(msg));
sleep(1);
}
}
else if (pid > 0) // 父进程
{
printf("我是父进程,我的PID为%d\n", getpid());
close(pipefd[1]); // 关闭父进程的写端
// 父进程不断读取数据到buf数组中,并打印
while (1)
{
char buf[100] = "";
int ret = read(pipefd[0], buf, sizeof(buf));
if (ret == -1)
{
perror("read");
}
else if (ret == 0)
{
break;
}
else
{
printf("父进程读取到的数据:%s\n", buf);
}
}
}
return 0;
}
10.2 命名管道(FIFO)
匿名管道只能用于具有亲缘关系(父子进程、兄弟进程)的进程间通信。因为匿名管道没有名字,即没有管道文件,只能通过fork来拷贝父进程打开的文件描述符,实现利用匿名管道的进程间通信。
为了解决匿名管道的这种问题,提出了命名管道(FIFO)。命名管道在不相关的进程间也能相互通信,因为命名管道以FIFO的文件形式存在于文件系统中,其打开方式和普通文件是一样的,只要可以使用管道文件,就可以相互通信。
不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出(FIFO)原则,不支持lseek之类的文件定位操作。这也是命名管道的另一个名称——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 文件权限
实现简单聊天功能:
A.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
int main()
{
// 判断有名管道fifo1是否存在,不存在则创建
int ret = access("fifo1", F_OK);
if (ret == -1)
{
printf("管道不存在,创建对应的有名管道\n");
ret = mkfifo("fifo1", 0664);
if (ret == -1)
{
perror("mkfifo");
exit(-1);
}
}
// 判断有名管道fifo2是否存在,不存在则创建
ret = access("fifo2", F_OK);
if (ret == -1)
{
printf("管道不存在,创建对应的有名管道\n");
ret = mkfifo("fifo2", 0664);
if (ret == -1)
{
perror("mkfifo");
exit(-1);
}
}
// 以只写的方式打开管道fifo1
int fdw = open("fifo1", O_WRONLY);
if (fdw == -1)
{
perror("open");
exit(-1);
}
printf("打开管道fifo1成功,等待写入...\n");
// 以只读的方式打开管道fifo2
int fdr = open("fifo2", O_RDONLY);
if (fdr == -1)
{
perror("open");
exit(-1);
}
printf("打开管道fifo2成功,等待读取...\n");
char buf[128];
// 循环地写读数据
while (1)
{
memset(buf, 0, 128);
// 获取标准输入的数据
fgets(buf, 128, stdin);
// 向管道中写数据
ret = write(fdw, buf, strlen(buf));
if (ret == -1)
{
perror("write");
exit(-1);
}
// 从管道中读数据
memset(buf, 0, 128);
ret = read(fdr, buf, 128);
if (ret <= 0)
{
perror("read");
break;
}
printf("buf: %s\n", buf);
}
// 关闭文件描述符
close(fdr);
close(fdw);
return 0;
}
B.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
int main()
{
// 判断有名管道fifo1是否存在,不存在则创建
int ret = access("fifo1", F_OK);
if (ret == -1)
{
printf("管道不存在,创建对应的有名管道\n");
ret = mkfifo("fifo1", 0664);
if (ret == -1)
{
perror("mkfifo");
exit(-1);
}
}
// 判断有名管道fifo2是否存在,不存在则创建
ret = access("fifo2", F_OK);
if (ret == -1)
{
printf("管道不存在,创建对应的有名管道\n");
ret = mkfifo("fifo2", 0664);
if (ret == -1)
{
perror("mkfifo");
exit(-1);
}
}
// 以只读的方式打开管道fifo1
int fdr = open("fifo1", O_RDONLY);
if (fdr == -1)
{
perror("open");
exit(-1);
}
printf("打开管道fifo1成功,等待读取...\n");
// 以只写的方式打开管道fifo2
int fdw = open("fifo2", O_WRONLY);
if (fdw == -1)
{
perror("open");
exit(-1);
}
printf("打开管道fifo2成功,等待写入...\n");
char buf[128];
// 循环地读写数据
while (1)
{
// 从管道中读数据
memset(buf, 0, 128);
ret = read(fdr, buf, 128);
if (ret <= 0)
{
perror("read");
break;
}
printf("buf: %s\n", buf);
memset(buf, 0, 128);
// 获取标准输入的数据
fgets(buf, 128, stdin);
// 向管道中写数据
ret = write(fdw, buf, strlen(buf));
if (ret == -1)
{
perror("write");
exit(-1);
}
}
// 关闭文件描述符
close(fdr);
close(fdw);
return 0;
}
Makefile:
.PHONY:all
all: A B
A:A.c
gcc $^ -o $@
B:B.c
gcc $^ -o $@
.PHONY:clean
clean:
rm -f A B
11. 进程间通信之共享内存
共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种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 指向管理共享内存信息的结构体
12. 进程间通信之信号
12.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 | 非法系统调用 |
12.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; // 微秒
};
12.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); // 已弃用
};
12.4 信号集
12.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;
12.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