Linux 进程、进程间通信和信号

1.进程相关知识

  • PCB进程控制块包含的信息
    • 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
    • 进程的状态,有就绪、运行、挂起、停止等状态。
    • 进程切换时需要保存和恢复的一些CPU寄存器。
    • 描述虚拟地址空间的信息。
    • 描述控制终端的信息。
    • 当前工作目录(Current Working Directory)。
    • umask掩码。
    • 文件描述符表,包含很多指向file结构体的指针。
    • 和信号相关的信息(未决信号集、信号屏蔽字)。
    • 用户id和组id。
    • 会话(Session)和进程组。
    • 进程可以使用的资源上限(Resource Limit)

具体更多操作系统相关的知识可以看这里的随笔  <操作系统 - 随笔分类 - imXuan - 博客园 (cnblogs.com)>

  • 进程组和会话:多个进程组成进程组,多个进程组组成会话(ps ajx 查看 进程组id 和 会话id)

2.进程创建

2.1 fork

  功能:用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。

  fork创建子进程,两个进程逻辑上虽然是完全用虚拟内存进行隔离的,但实际上linux引入了读时共享,写时复制的原则,共同读取的数据不需要复制,需要写入的时候再复制,节省空间,具体可以参考操作系统随笔中的内容

#include <sys/types.h>
#include <unistd.h>
​
pid_t fork(void);
/*
返回值:
    成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为整型。
    失败:返回-1。
        失败的两个主要原因是:
            1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。
            2)系统内存不足,这时 errno 的值被设置为 ENOMEM。
*/

2.2 getpid

  功能:获取本进程号(PID)

#include <sys/types.h>
#include <unistd.h>
​
pid_t getpid(void);
// 返回值:本进程号

2.3 getppid

  功能:获取调用此函数的进程的父进程号(PPID)

#include <sys/types.h>
#include <unistd.h>
​
pid_t getppid(void);
// 返回值:调用此函数的进程的父进程号(PPID)

2.4 getpgid

  功能:获取进程组号(PGID)

#include <sys/types.h>
#include <unistd.h>
​
pid_t getpgid(pid_t pid);
/*    
    参数:pid:进程号
    返回值:参数为 0 时返回当前进程组号,否则返回参数指定的进程的进程组号
*/

2.5 exec 函数族

  将当前进程的代码段、数据段等替换成所需要加载程序的代码段、数据段,从新的代码段的第一条指令开始执行,但进程ID不变

  exec函数族函数一旦调用成功,不会返回值,只有失败才返回 -1 或 errno

2.5.1 execlp

int execlp(const char* file, const char* arg, ... /* (char*) NULL */);
/*
参数
   file: 加载程序的名字,需要配合环境变量 PATH 使用
   arg0: 可执行文件名
   arg1: 参数
    ...
   argn: NULL (哨兵)  
*/

  示例

execlp("ls", "ls", "-l", "-F", "-a", NULL); 

  补充

int main( int argc, char* argv[])
{ 
     //函数体内使用了argc或argv
     ……
     return 0;
}
//    argv[0]指向程序运行的全路径名 
//    argv[1]指向在命令行中执行程序名后的第一个字符串 
//    argv[2]指向执行程序名后的第二个字符串 

2.5.2 execl

int execlp(const char* file, const char* arg, ... /* (char*) NULL */);
/*
参数
   file: 加载程序的绝对路径的程序名字
   arg0: 可执行文件名
   arg1: 参数
    ...
   argn: NULL (哨兵)  
*/

  示例

execl("./bin/ls", "ls", "-l", "-F", "-a", NULL); 

3.进程回收

  • 父进程有义务在子进程结束时,回收该子进程,隔备进程无回收关系
  • 进程终止:
    • 关闭所有文件描述符
    • 释放用户空间分配的内存
    • 进程的 pcb 残留在内核。保存进程结束的状态(正常:退出值。异常:终止其运行的信号编号)

3.1 孤儿进程

  父进程先于子进程终止,子进程沦为“孤儿进程”,会被 init 进程领养

  ps ajx 指令可以查看进程信息

3.2 僵尸进程(zombie)

  子进程终止,父进程未终止,但父进程尚未对子进程进行回收

  结束进程指令:kill -9 进程id。只能结束活跃进程,僵尸进程无效,僵尸进程已经结束,只是父进程没有把他干掉,PCB残留在内核中

3.3 wait 回收

  • 功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。
  • 作用
    • <阻塞>等待子进程退出(终止)
    • 回收子进程残留在内核的pcb
    • 获取子进程的退出状态(正常、异常),传出参数:status
#include <sys/types.h>
#include <sys/wait.h>
​
pid_t wait(int *status);
/*
    参数:
        status : 进程退出时的状态信息。
    返回值:
        成功:已经结束子进程的进程号
        失败: -1
*/

  示例,通过宏可以获取退出码或者信号编号,也可以传入NULL,不需要保存任何信息,只是把子进程回收

int main(int argc, char *argv[])
{
    int status = 9;
    pid_t wpid = 0;
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork err"); exit(1);
    }
    else if(pid == 0)
    {
        printf("I'm child pid = %d\n", getpid());
        sleep(3);
        exit(66);
    }
    else
    {
        wpid = wait(&status);    // 保存子进程退出的状态
        if(wpid == -1)
        {
            perror("wait err"); exit(1);
        } 
        if(WIFEXITED(status))    // 宏函数为真,说明子进程正常退出
        {    // 获取退出码
            printf("I'm parent, pid = %d child, exit code = %d\n", wpid, WEXITSTATUS(status));
        }
        else if(WIFSIGNALED(status))    // 宏函数为真, 说明子进程被信号终止
        {    // 获取信号编码
            printf("I'm parent, pid = %d child, killed by %d signal\n", wpid, WTERMSIG(status));
        }
    }
    return 0;
}

3.4 waitpid 回收

pid_t waitpid(pid_t pid, int* status, int options);
/*
     pid > 0  等待进程为 pid 的子进程。
     pid = 0  等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。
     pid = -1 等待任一子进程,此时 waitpid 和 wait 作用一样。
     pid < -1 等待指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。

     status: 传出回收子进程状态
     options: WNOHANG -- 指定回收方式为 “非阻塞”;  0 为阻塞方式
     
     成功返回回收进程的 pid , 失败返回 -1 , 子进程未结束则返回 0 (用非阻塞回收)
*/

  **注意:一次 wait 、 waitpid  调用只能回收一个子进程,想回收 N 个子进程需要将函数放于循环中

4.进程间通信

  • 进程间通信的原理,多个进程虽然对应了多个虚拟内存映射,但是系统内核是相同的,可以通过内核传递数据
  • 进程间通信的方法
    • 1.管道(最简单)
    • 2.信号(开销小)
    • 3.mmap 映射(速度快,非血缘关系)
    • 4.socket 本地套接字(稳定性好)

4.1 pipe(匿名管道)

  • 实现原理:Linux 内核使用环形队列机制,借助缓冲器(4k)实现
  • 特质
    • 本质:伪文件(实际是内核缓冲区)
    • 用于进程间通信,由两个文件描述符引用,一个读端,一个写端
    • 规定数据从管道写端流入,从读端流出
  • 局限性
    • 只能自己写,不能自己读
    • 管道中的数据,读走就销毁,不能反复读取
    • 半双工通信,数据在同一时刻只能在一个方向上流动
    • 应用于血缘关系进程间
#include <unistd.h>
int pipe(int pipefd[2]);
// pipefd : 传入传出参数,其存放了管道的文件描述符 
// 管道读端: pipefd[0]
// 管道写端: pipefd[1]
// 返回值: 成功0; 失败-1, errno
  • 示例
int main()
{
    int fd_pipe[2] = { 0 };
    pid_t pid;
    if (pipe(fd_pipe) < 0)  // 创建管道
        perror("pipe");
    pid = fork(); // 创建进程
    if (pid == 0)
    { // 子进程
        char buf[] = "I am mike";
        write(fd_pipe[1], buf, strlen(buf));  // 往管道写端写数据
        _exit(0);
    }
    else if (pid > 0)
    {// 父进程
        wait(NULL); // 等待子进程结束,回收其资源
        char str[50] = { 0 };
        read(fd_pipe[0], str, sizeof(str));  // 从管道里读数据
        printf("str=[%s]\n", str); // 打印数据
    }
    return 0;
}

 管道读写行为 

  • 读管道
    • 有数据:read返回实际读到的字节数
    • 无数据:有写端阻塞;无写端返回0(没有相应的read函数)
  • 写管道
    • 无读端:异常终止(没有相应的write函数)(SIGPIPE信号)
    • 有读端:管道满阻塞,管道未满返回实际写入字节数

4.2 fifo

  • 创建一个FIFO管道,可以使用 open 函数等系统调用打开
int mkfifo(const char *pathname, mode_t mode);
/*    参数:
            pathname : 普通的路径名,也就是创建后 FIFO 的名字。
            mode : 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同。(0666)
        返回值:
            成功:0   状态码
            失败:如果文件已经存在,则会出错且返回 -1
*/
  • 已经创建一个FIFO管道后,举例使用FIFO文件进行读写操作
//进行1,写操作
int fd = open("my_fifo", O_WRONLY);  
​
char send[100] = "Hello Mike";
write(fd, send, strlen(send));
​
//进程2,读操作
int fd = open("my_fifo", O_RDONLY); //等着只写  
char recv[100] = { 0 };
//读数据,命名管道没数据时会阻塞,有数据时就取出来  
read(fd, recv, sizeof(recv));
printf("read from my_fifo buf=[%s]\n", recv);

4.3 mmap / munmap

  • mmap: 借助文件映射,创建共享内存应摄取
#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
/*  参数:
        addr :  指定映射的起始地址, (设为NULL则由系统指定)
        length:映射到内存的文件长度 (length <= 文件的大小)
        prot:  映射区的保护方式, 最常用的 :
            a) 读:PROT_READ
            b) 写:PROT_WRITE
            c) 读写:PROT_READ | PROT_WRITE
        flags:  标注共享内存映射区的特性
            a) MAP_SHARED : 写入内存映射区的数据会复制回文件, 且允许其他映射该文件的进程共享
            b) MAP_PRIVATE : 对映射区的写入操作会产生一个映射区的复制(copy - on - write), 对此区域所做的修改不会写回原文件
        fd:由open返回的文件描述符, 代表要映射的文件
        offset:以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射
    返回值:
        成功:返回创建的映射区首地址
        失败:MAP_FAILED宏
*/
  • munmap: 释放内存映射区
#include <sys/mman.h>int munmap(void *addr, size_t length);
/*  参数:
        addr:使用mmap函数创建的映射区的首地址
        length:映射区的大小
    返回值:
        成功:0
        失败:-1
*/
  • mmap 中有一个 falgs 是 O_ANONYMOUS ,允许建立一个匿名映射,也就是不需要额外传入一个文件描述符来创建映射区,但这种方式没办法在没有血缘关系的进程间通信

4.4 本地套接字

网络套接字函数  Linux TCP/UDP socket 通信和IO多路复用

4.5 shmget

  • 通过key创建共享内存
int shmget(key_t key, size_t size, int shmflg);
/*  参数:
        key: 通过该key记录共享内存在内核中的位置, 需要>0
        size: 创建时指定内存大小, 打开时写0就行
        flags:
            IPC_CREAT: 创建共享内存
                IPC_CREAT | 0664 创建共享内存的时候设置操作权限
            IPC_CREAT | IPC|EXCL: 检测是否存在, 存在返回-1, 不存在返回0
    返回值:
        成功: 得到整形数 (对应这块共享内存)
        失败: -1           */
int shmid = shmget(100, 4096, IPC_CREAT | 0664);    // 创建共享内存
int shmid = shmget(100, 0, 0);    // 打开共享内存
  • 将进程和共享内存关联
void* shmat(int shmid, const void* shmaddr, int shmflg);
/*  参数
        shmid: 通过 shmget 的返回值访问共享内存
        shmaddr: 指定共享内存在内核中的位置, NULL->委托内核分配
        shmflg: 关联成功后对共享内存的操作权限
            SHM_RDONLY: 只读
            0: 读写
    返回值
        成功: 共享内存的地址
        失败: (void*) -1                */
void* ptr = shmat(shmid, NULL, 0);    // 获取共享内存的指针
memcpy(ptr, "shared memory test", len);    // 写内存
printf("%s\n", (char*)ptr);    // 读内存
  • 将共享内存和当前进程分离
int shmdt(const void* shmaddr);
/*  参数: 共享内存的起始地址, shmat的返回值
    返回值: 成功0, 失败-1            */
  • 共享内存操作
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*  参数
        shmid: shmget() 的返回值
        cmd: 对共享内存的操作
            IPC_STAT: 获取共享内存状态
            IPC_SET: 设置共享内存状态
            IPC_RMID: 标记共享内存要被销毁 (所有进程引用结束后)
        buf: 为第二个参数服务
            cmd==IPC_STAT: 获取内存状态信息
            cmd==IPC_SET: 设置内存状态信息
            dmd==IPC_RMID: 目的是删除, 该参数NULL
    返回值:
        成功0 失败-1             */
shmctl(shmid, IPC_RMID, NULL);    // 删除共享内存

  如果标记了删除共享内存,共享内存的 key_t 会被修改为0, 也就是不能有新的进程再关联该块共享内存, 接下来等到所有关联该块共享内存的进程结束后, 共享内存会被回收

4.6 shm和mmap对比

  1. shm 共享内存可以直接创建,mmap 内存映射区需要以来磁盘文件
  2. shm 效率更高
    • shm 直接对内存操作
    • mmap 映射需要同步磁盘文件(首次仅建立映射,读哪里哪里缺页才从磁盘拷贝到内存,内存内容改变后一段时间会写入到磁盘文件,也可以msync()强制同步到文件
  3. 内存共享
    • shm 所有进程共享一块内存
    • mmap 每个进程都会在自己的虚拟地址空间有一块独立的内存,通过磁盘文件映射
  4. 数据安全性
    • 进程突然退出
      • shm 在内核中还会存在
      • mmap 在进程的虚拟地址空间会直接消失
    • 电脑死机的话由于 mmap 一部分关联磁盘文件,还是会保留一些
  5. 生命周期
    • shm 进程退出,共享内存还在,需要手动调用函数 shmctl(shmid, IPC_RMID, NULL); 删除或者重启电脑
    • mmap 进程退出,虚拟地址空间销毁,内存映射区也会销毁

5.信号

5.1 信号相关概念

  • 未决:产生与递达(处理)之间的状态。该状态主要受阻塞(屏蔽)影响
  • 递达:内核产生信号后递送并且成功到达进程。递达的信号会被内核立即处理
  • 信号处理方式:
    • 执行默认动作
    • 忽略(丢弃)
    • 捕捉(调用用户指定的函数)
  • 阻塞信号集(信号屏蔽字)
    • 本质:位图。用来记录信号屏蔽状态
    • 该信号集中的信号表示成功被设置屏蔽。再次收到该信号,其处理动作将延后至解除屏蔽。在此期间该信号一直处于未决态
  • 未决信号集
    • 本质:位图,用来记录信号的处理状态
    • 该信号集中的信号表示信号已经产生,但尚未处理

5.2 信号4要素

  • 信号四要素的内容(man 7 signal)
    • 编号
    • 名称
    • 事件
    • 默认处理动作
编号信号对应事件默认动作
1 SIGHUP 用户退出shell时,由该shell启动的所有进程将收到这个信号 终止进程
2 SIGINT 当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号 终止进程
3 SIGQUIT 用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号 终止进程
4 SIGILL CPU检测到某进程执行了非法指令 终止进程并产生core文件
5 SIGTRAP 该信号由断点指令或其他 trap指令产生 终止进程并产生core文件
6 SIGABRT 调用abort函数时产生该信号 终止进程并产生core文件
7 SIGBUS 非法访问内存地址,包括内存对齐出错 终止进程并产生core文件
8 SIGFPE 在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误 终止进程并产生core文件
9 SIGKILL 无条件终止进程。本信号不能被忽略,处理和阻塞 终止进程,可以杀死任何进程
10 SIGUSE1 用户定义的信号。即程序员可以在程序中定义并使用该信号 终止进程
11 SIGSEGV 指示进程进行了无效内存访问(段错误) 终止进程并产生core文件
12 SIGUSR2 另外一个用户自定义信号,程序员可以在程序中定义并使用该信号 终止进程
13 SIGPIPE Broken pipe向一个没有读端的管道写数据 终止进程
14 SIGALRM 定时器超时,超时的时间 由系统调用alarm设置 终止进程
15 SIGTERM 程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号 终止进程
16 SIGSTKFLT Linux早期版本出现的信号,现仍保留向后兼容 终止进程
17 SIGCHLD 子进程结束时,父进程会收到这个信号 忽略这个信号
18 SIGCONT 如果进程已停止,则使其继续运行 继续/忽略
19 SIGSTOP 停止进程的执行。信号不能被忽略,处理和阻塞 为终止进程
20 SIGTSTP 停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号 暂停进程
21 SIGTTIN 后台进程读终端控制台 暂停进程
22 SIGTTOU 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生 暂停进程
23 SIGURG 套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达 忽略该信号
24 SIGXCPU 进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程 终止进程
25 SIGXFSZ 超过文件的最大长度设置 终止进程
26 SIGVTALRM 虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间 终止进程
27 SGIPROF 类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间 终止进程
28 SIGWINCH 窗口变化大小时发出 忽略该信号
29 SIGIO 此信号向进程指示发出了一个异步IO事件 忽略该信号
30 SIGPWR 关机 终止进程
31 SIGSYS 无效的系统调用 终止进程并产生core文件
34~64 SIGRTMIN ~ SIGRTMAX LINUX的实时信号,它们没有固定的含义(可以由用户自定义) 终止进程

5.3 kill

  • 功能:给指定进程发送指定信号(不一定杀死)
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
/*  参数:
        pid : 取值有 4 种情况 :
            pid > 0:  将信号传送给进程 ID 为pid的进程。
            pid = 0 :  将信号传送给当前进程所在进程组中的所有进程。
            pid = -1 : 将信号传送给系统内所有的进程。
            pid < -1 : 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝对值。
        sig : 信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill - l("l" 为字母)进行相应查看。不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
​
    返回值:
        成功:0
        失败:-1
*/

5.4 alarm

  • 功能:设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送 (14)SIGALRM 信号。进程收到该信号,默认动作是终止,也可以单独设置处理函数。每个进程都有且只有唯一的一个定时器。
#include <unistd.h>
​
unsigned int alarm(unsigned int seconds);
/*  取消定时器alarm(0),返回旧闹钟余下秒数。
    参数:
        seconds:指定的时间,以秒为单位
    返回值:
        返回0或剩余的秒数
​*/

5.5 setitimer

  • 功能:设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
#include <sys/time.h>int setitimer(int which,  const struct itimerval *new_value, struct itimerval *old_value);
/*  参数:
        which:指定定时方式
            a) 自然定时:ITIMER_REAL → (14)SIGALRM计算自然时间
            b) 虚拟空间计时(用户空间):ITIMER_VIRTUAL → (26)SIGVTALRM  只计算进程占用cpu的时间
            c) 运行时计时(用户 + 内核):ITIMER_PROF → (27)SIGPROF计算占用cpu及执行系统调用的时间
        new_value:传入参数,struct itimerval, 负责设定timeout时间
        old_value:传出参数,存放旧的timeout值,一般指定为NULL
    返回值:
        成功:0
        失败:-1
*/
  • itermerval 结构体
struct itimerval {
      struct timerval it_interval; // 闹钟触发周期
      struct timerval it_value;    // 闹钟触发时间
};
struct timeval {
      long tv_sec;            //
      long tv_usec;           // 微秒
}
// itimerval.it_value:设定第一次执行function所延迟的秒数 
// itimerval.it_interval:  设定以后每几秒执行function
  • 信号处理,使用该函数可以指定信号和对应的处理方法
signal(SIGALRM, myfunc);

5.6 信号集操作函数

  • 未决信号集上的位置为1时,内核会递达对应的动作并进行处理,但可以设置阻塞信号集为 1 ,组织内核处理未决信号集,直到阻塞信号设为 0 后内核再进行处理
#include <signal.h>  

sigset_t set; // 自定义信号集​

int sigemptyset(sigset_t *set);       // 将自定义信号集 set 置空
int sigfillset(sigset_t *set);          // 将所有信号加入自定义信号集 set 
int sigaddset(sigset_t *set, int signo);  // 将 signo 信号加入到自定义信号集 set
int sigdelset(sigset_t *set, int signo);   // 从自定义集合 set 中移除 signo 信号
int sigismember(const sigset_t *set, int signo); // 判断信号是否存在
int sigpending(sigset_t *set);        // 返回当前进程的未决信号集

5.6.1 sigprocmask 修改信号阻塞集

  • 功能: 检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由 set 指定,而原先的信号阻塞集合由 oldset 保存。
#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/*参数:
    how : 信号阻塞集合的修改方法,有 3 种情况:
        SIG_BLOCK:向信号阻塞集合中添加 set 信号集,新的信号掩码是set和旧信号掩码的并集。相当于 mask = mask|set。
        SIG_UNBLOCK:从信号阻塞集合中删除 set 信号集,从当前信号掩码中去除 set 中的信号。相当于 mask = mask & ~ set。
        SIG_SETMASK:将信号阻塞集合设为 set 信号集,相当于原来信号阻塞集的内容清空,然后按照 set 中的信号重新设置信号阻塞集。相当于mask = set。
    set : 要操作的信号集地址。
        若 set 为 NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到 oldset 中。
    oldset : 保存原先信号阻塞集地址
​
返回值:
    成功:0,
    失败:-1,失败时错误代码只可能是 EINVAL,表示参数 how 不合法。
*/

5.6.2 signal 信号处理

  • 功能: 注册信号处理函数(不可用于 SIGKILL、SIGSTOP 信号),即确定收到信号后处理函数的入口地址。此函数不会阻塞。
#include <signal.h>
​
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
​
/*参数:
    signum:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill - l("l" 为字母)进行相应查看。
    handler : 取值有 3 种情况:
          1. SIG_IGN:忽略该信号
          2. SIG_DFL:执行系统默认动作
          3. 信号处理函数名:自定义信号处理函数,如:func
          回调函数的定义如下:
          void func(int signo)
          {
              // signo 为触发的信号,为 signal() 第一个参数的值
          }
返回值:
    成功:第一次返回 NULL,下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型。
    失败:返回 SIG_ERR
*/

5.6.3 sigaction 信号处理

  • 功能:检查或修改指定信号的设置(或同时执行这两种操作)
#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
/*参数:
    signum:要操作的信号。
    act:   要设置的对信号的新处理方式(传入参数)。
    oldact:原来对信号的处理方式(传出参数)。
    如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非空,则系统将此前指定信号的处理方式存入 oldact。
​
返回值:
    成功:0, 失败:-1, errno
*/
  • sigaction 结构体
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表示默认,默认屏蔽本信号,避免正在处理的时候又来了一个这个信号,然后立刻又开始调用处理函数,注意屏蔽不是丢失这个信号,但它是一个位图,只能是记录又有一个信号来,但不能计数
    void(*sa_restorer)(void); // 已弃用
};
  • 注意信号捕捉的一些特性
    • sa_mask 只是捕捉函数期间生效的信号阻塞集
    • sa_flags = 0 只是捕捉函数执行期间自动屏蔽本信号
    • 如果 sa_flags=0 ,它会加入到未决信号集,但由于这是一个位图,所以解除屏蔽后只会执行一次

5.6.4 借助信号捕捉回收子进程

  •  一个信号捕捉的实例
#include <signal.h>     // 信号
#include <stdio.h>      // 标注输入输出
#include <unistd.h>     // fork函数
#include <sys/wait.h>   // wait函数

void sig_child(int signo)
{
    pid_t wpid;
    int status;
    // if((wpid = wait(NULL))!= -1) // 如果这里写 if 可能会因为处理过程中有多个信号发送少回收很多子进程产生僵尸进程
    while((wpid = waitpid(-1, &status, 0)) != -1)
    {
        printf("--- catch child pid = %d, ret = %d ---\n", wpid, WEXITSTATUS(status));
    }
}

int main()
{
    pid_t pid;
    int i;

    for(i = 0; i<15; i++) {
        if((pid = fork())==0)
            break;
    }

    if(i == 15) { // 父进程执行
        struct sigaction act;       // 创建一个 sigaction 结构体

        act.sa_handler = sig_child; // 设置回调函数
        sigemptyset(&act.sa_mask);  // 设置执行时的信号阻塞集为空
        act.sa_flags = 0;           // 设置信号处理方式为默认

        sigaction(SIGCHLD, &act, NULL);

        printf("i am parent, pid = %d\n", getpid());
        while(1);
    } else {      // 子进程执行
        printf("i am child, pid = %d\n", getpid());
        return i;
    }
    return 0;
}

 

posted @ 2023-10-28 19:42  imXuan  阅读(27)  评论(0编辑  收藏  举报