C语言 多进程编程(三)信号处理方式和自定义处理函数

简介: 本文详细介绍了Linux系统中进程间通信的关键机制——信号。首先解释了信号作为一种异步通知机制的特点及其主要来源,接着列举了常见的信号类型及其定义。文章进一步探讨了信号的处理流程和Linux中处理信号的方式,包括忽略信号、捕捉信号以及执行默认操作。此外,通过具体示例演示了如何创建子进程并通过信号进行控制。最后,讲解了如何通过`signal`函数自定义信号处理函数,并提供了完整的示例代码,展示了父子进程之间通过信号进行通信的过程。

进程间通信之信号

信号

信号是在软件层次上 是⼀种通知机制, 对中断机制的⼀种模拟,是⼀种异步通信⽅式, ⼀般具有
如下特点:

1. 进程在运⾏过程中,随时可能被各种信号打断
2. 进程可以忽略, 或者去调⽤相应的函数去处理信号
3.进程⽆法预测到达的精准时间

在 Linux 中信号⼀般的来源如下

程序执⾏错误,如内存访问越界,数学运算除 0

由其他进程发送

通过控制终端发送 如 ctrl + c

⼦进程结束时向⽗进程发送的 SIGCLD 信号

程序中设定的定时器产⽣的 SIGALRM 信号

信号的种类

在 Linux 系统可以通过 kill -l 命令查看, 常⽤的信号列举如下

img_34.png

  • SIGINT 该信号在⽤户键⼊ INTR 字符 (通常是 Ctrl-C) 时发出,终端驱动程序发送此
    信号并送到前台进>程中的每⼀个进程。

  • SIGQUIT 该信号和 SIGINT 类似,但由 QUIT 字符 (通常是 Ctrl-) 来控制。

  • SIGILL 该信号在⼀个进程企图执⾏⼀条⾮法指令时 (可执⾏⽂件本身出现错误,或者
    试图执⾏数据段、堆栈溢出时) 发出。

  • SIGFPE 该信号在发⽣致命的算术运算错误时发出。这⾥不仅包括浮点运算错误,还
    包括溢出及除数 > 为 0 等其它所有的算术的错误。

  • SIGKILL 该信号⽤来⽴即结束程序的运⾏,并且不能被阻塞、处理和忽略。

  • SIGALRM 该信号当⼀个定时器到时的时候发出。

  • SIGSTOP 该信号⽤于暂停⼀个进程,且不能被阻塞、处理或忽略。

  • SIGTSTP 该信号⽤于交互停⽌进程,⽤户可键⼊ SUSP 字符时 (通常是 Ctrl-Z) 发出
    这个信号。

  • SIGCHLD ⼦进程改变状态时,⽗进程会收到这个信号

  • SIGABRT 进程异常中⽌

信号在操作系统中的定义如下:

#define SIGHUP       1
#define SIGINT       2
#define SIGQUIT      3
#define SIGILL       4
#define SIGTRAP      5
#define SIGABRT      6
#define SIGIOT       6
#define SIGBUS       7
#define SIGFPE       8
#define SIGKILL      9 
#define SIGUSR1     10 // 用户自定义信号
#define SIGSEGV     11
#define SIGUSR2     12
#define SIGPIPE     13
#define SIGALRM     14
#define SIGTERM     15
#define SIGSTKFLT   16
#define SIGCHLD     17
#define SIGCONT     18
#define SIGSTOP     19
#define SIGTSTP     20
#define SIGTTIN     21

信号的处理流程

  • 信号的发送 :可以由进程直接发送

  • 信号投递与处理 : 由内核进⾏投递给具体的进程并处理

    在 Linux 中对信号的处理⽅式

  • 忽略信号, 即对信号不做任何处理,但是有两个信号不能忽略:即 SIGKILL 及
    SIGSTOP。

  • 捕捉信号, 定义信号处理函数,当信号发⽣时,执⾏相应的处理函数。

  • 执⾏缺省操作,Linux 对每种信号都规定了默认操作

img_35.png

内核通过task_struct找到相应的进程,然后将信号的类型和进程号传递给信号处理函数。信号处理函数根据信号类型做相应的处理。

在内核中的⽤于管理进程的结构为 task_struct , 具体定义如下:
img_36.png

任务队列

内核把进程的列表存放在叫做任务队列(task list) 的双向循环链表中。链表中的每一 项都是类型为task_struct

备注:有些操作系统会把任务队列称为任务数组。但是Linux实现时使用的是队列而不是静态数组,所以称为任务队列

https://blog.csdn.net/qq_41453285/article/details/103743235
更多关于task_struct 的信息,请参考《深入理解LINUX内核》

记录进程信号和相应的处理方式
img_37.png

自定义信号处理函数

这种方式需要在程序中编写信号处理函数,并在程序内核中注册信号处理函数。

信号的发送

当由进程来发送信号时, 则可以调⽤ kill() 函数与 raise () 函数

kill() 函数:

用于向指定进程发送信号

函数头文件:

#include <signal.h>
#include <sys/types.h>

原型如下:

int kill(pid_t pid, int sig);

参数:

pid_t pid: 进程ID
int sig: 信号值

返回值:

- 成功: 0
- 失败: -1  并设置 errno

raise() 函数:

用于向当前进程发送信号

函数头文件:

#include <signal.h>
#include <sys/types.h>

原型如下:

int raise(int sig);

参数:

int sig: 信号值

返回值:

- 成功: 0
- 失败: -1  并设置 errno

示例 : 创建⼀个⼦进程,⼦进程通过信号暂停,⽗进程发送 终⽌信号

/*
 * 创建⼀个⼦进程,⼦进程通过信号暂停,⽗进程发送 终⽌信号
 * */

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

int main(){
   
   
    pid_t child_pid; // ⼦进程ID  pid_t 类型在<sys/types.h>是⼀个整数类型,用来存储进程ID, 它是系统中⼀个进程的唯一标识符。系统中每个进程都有⼀个独⽴的pid。
    child_pid = fork(); // 创建⼀个⼦进程,⼦进程复制⽗进程的地址空间,并返回⼦进程的pid。
    if (child_pid ==-1){
   
    // 创建失败
        perror("fork");// 输出错误信息
        exit(EXIT_FAILURE);// 退出程序
    } else if (child_pid == 0) {
   
    //只在⼦进程运行的代码
        //fprintf和printf的区别在于fprintf可以指定输出到哪个文件,printf默认输出到标准输出。
        //stdout是标准输出,输出到屏幕上,还有stderr是错误输出,输出到屏幕上。stdin是标准输入,输入从键盘上。
        fprintf(stdout, "子进程正在运行...子进程ID:%d\n", getpid());

        raise(SIGSTOP); // 发送SIGSTOP信号给⼦进程自己,暂停⼦进程的运行。

        fprintf(stdout, "子进程暂停自己后被父进程信号kill,不会打印这句话:%d\n", getpid());
        exit(EXIT_SUCCESS); // 退出⼦进程
    } else if (child_pid > 0) {
   
    // 父进程运行的代码
        int ret;
        sleep(2); // 父进程休眠2秒,等待⼦进程

        ret = kill(child_pid, SIGKILL);// 发送SIGKILL信号给⼦进程,终⽌⼦进程。
        //SIGKILL信号是强制终⽌进程的信号,它会杀死进程,并释放资源, 但是它不能被捕获和处理。

        if (ret == -1){
   
    // 发送失败
            perror("kill");// 输出错误信息
            exit(EXIT_FAILURE);//退出程序
        } else {
   
   
            fprintf(stdout, "父进程终⽌⼦进程成功!\n");
            wait(NULL); // 等待⼦进程结束,防止⼦进程僵死。
            //wait函数传入NULL,表示等待任意⼦进程结束,返回值是⼦进程的终⽌状态。
        }
    }

    return 0;
}

运行结果:

子进程正在运行...子进程ID:3957
父进程终⽌⼦进程成功!

等待信号

在进程没有结束时,进程在任何时间点都可以接受到信号

需要阻塞等待信号时,则可以调⽤ pause() 函数

pause() 函数:

用于进程暂停,直到收到信号

函数头文件:

#include <signal.h>

原型如下:

int pause(void);

参数:

返回值:

- 成功: 0
- 失败: -1  并设置 errno

示例 : 创建创建⼀个⼦进程, ⽗进程调⽤ pause 函数,⼦进程给⽗进程发送信号

int main(){
   
   
    pid_t child_pid; // ⼦进程ID  pid_t 类型在<sys/types.h>是⼀个整数类型,用来存储进程ID, 它是系统中⼀个进程的唯一标识符。系统中每个进程都有⼀个独⽴的pid。
    child_pid = fork(); // 创建⼀个⼦进程,⼦进程复制⽗进程的地址空间,并返回⼦进程的pid。
    if (child_pid ==-1){
   
    // 创建失败
        perror("fork");// 输出错误信息
        exit(EXIT_FAILURE);// 退出程序
    } else if (child_pid == 0) {
   
    //只在⼦进程运行的代码
        //fprintf和printf的区别在于fprintf可以指定输出到哪个文件,printf默认输出到标准输出。
        //stdout是标准输出,输出到屏幕上,还有stderr是错误输出,输出到屏幕上。stdin是标准输入,输入从键盘上。
        fprintf(stdout, "子进程正在运行...子进程ID:%d\n", getpid());
        sleep(1); // ⼦进程休眠1秒

        kill(getppid(), SIGUSR1); // 发送SIGUSR1信号(用户自定义信号1)给父进程,这个信号默认是结束进程

        fprintf(stdout, "发送SIGUSR1信号(用户自定义信号1)给父进程:%d\n", getpid());
        exit(EXIT_SUCCESS); // 退出⼦进程
    } else if (child_pid > 0) {
   
    // 父进程运行的代码

        fprintf(stdout, "父进程...父进程ID:%d\n", getpid());
        pause(); // 父进程阻塞,等待信号

        fprintf(stdout, "父进程...父进程收到信号");

        wait(NULL);
    }

    return 0;
}

运行结果:

父进程...父进程ID:4782
子进程正在运行...子进程ID:4783
发送SIGUSR1信号(用户自定义信号1)给父进程:4783

pause 函数⼀定要在收到信号之前调⽤,让进程进⼊到睡眠状态

信号的处理

信号是由操作系统内核发送给指定进程, 进程收到信号后则需要进⾏处理

处理信号三种⽅式:

  • 忽略 : 不进⾏处理
  • 默认 : 按照信号的默认⽅式处理
  • ⽤户⾃定义 : 通过⽤户实现⾃定义处理函数来处理,由内核来进⾏调⽤

    三种方式都是内核来处理.
    ⾃定义处理函数:需要将信号处理函数地址注册到内核中, 并在信号发⽣时, 由内核调用相应的处理函数。


对于每种信号都有相应的默认处理⽅式

进程退出:

SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,

SIGUSR1,SIGUSR2,SIGVTALRM

进程忽略

SIGCHLD,SIGPWR,SIGURG,SIGWINCH

进程暂停

SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU


⽤户⾃定义处理基本的流程

一. 实现⾃定义处理函数

⽤户实现⾃定义处理函数, 需要按照下⾯的形式定义

typedef void (*sighandler_t)(int);

typedef void (*)(int) sighandler_t
//sighandler_t 是信号处理函数的类型, 它是一个函数指针, 指向信号处理函数的起始地址。

二.设置信号处理处理⽅式

通过 signal 函数设置信号处理⽅式

函数头⽂件

#include <signal.h>

函数原型

sighandler_t signal(int signum, sighandler_t handler);

//sighandler_t 是信号处理函数的类型, 它是一个函数指针, 指向信号处理函数的起始地址。

函数功能

设置信号的处理⽅式, 如果是⾃定义处理⽅式,提供函数地址,注册到内核中

函数参数

signum : 信号编号 

handler : 信号处理⽅式
    - SIG_IGN (1): 忽略信号//信号处理函数不做任何事情
    - SIG_DFL (0): 按照默认⽅式处理//信号处理函数是系统默认的处理函数
    - 其他 : 自定义处理函数的地址//信号处理函数是⾃定义的处理函数

三种处理⽅式互斥,一般选择一种即可。

返回值

成功 : 信号处理函数的地址

失败 : 返回 SIG_ERR (-1) 并设置 errno

img_38.png

示例: 创建⼀个⼦进程, ⽗进程给⼦进程发送 SIGUSR1 信号,并使⽤⾃定义的处理函数处理信号

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
/*
 * 创建⼀个⼦进程, ⽗进程给⼦进程发送 SIGUSR1 信号,并使⽤⾃定义的处理函数处理信号
 *
 * */

//信号处理函数,通过signal函数关联对应的信号
//@param  sign 当前接受到的信号(与这个处理函数相关联的)
void sig_handler(int sign);

int main(int argc, char *argv[]) {
   
   
    __sighandler_t ret;//信号处理函数的返回值
    ret= signal(SIGUSR1, sig_handler);//关联信号处理函数
    if(ret==SIG_ERR){
   
   //出错处理
        perror("signal");//出错处理
        exit(1);//退出程序
    }
    //成功返回的信号处理函数指针

    //创建⼀个⼦进程
    pid_t pid=fork();
    if(pid==-1){
   
   //出错处理
        perror("fork");//出错处理
        exit(1);//退出程序
    }else if(pid==0){
   
   //⼦进程
        printf("⼦进程开始\n");
        //使⽤⾃定义的处理函数处理信号
        pause();
        //函数处理后回到子进程,继续执行
        printf("⼦进程结束\n");

    }else{
   
   //⽗进程
        sleep(1);//等待⼦进程启动
        printf("⽗进程发送信号\n");
        //给⼦进程发送 SIGUSR1 信号
        //信号投递是由内核完成,通过task_struct找到对应的进程,再去调用信号处理函数
        kill(pid, SIGUSR1);
        //等待⼦进程结束
        wait(NULL);
    }
    return 0;
}
//信号处理函数
void sig_handler(int sign){
   
   
    //处理信号
    printf("信号处理函数运行 %s\n", strsignal(sign));//strsignal函数将信号转换为字符串,返回一个字符串,描述信号编号的含义
}

运行结果:

⼦进程开始
⽗进程发送信号
信号处理函数运行 User defined signal 1
相关文章
|
7天前
|
安全 C语言
C语言中的字符、字符串及内存操作函数详细讲解
通过这些函数的正确使用,可以有效管理字符串和内存操作,它们是C语言编程中不可或缺的工具。
178 15
|
6月前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
320 23
|
1月前
|
Shell Linux C语言
函数和进程之间的相似性
在一个C程序可以fork/exec另一个程序,其过程是先fork一个子进程,然后让子进程使用exec系列函数将子进程的代码和数据替换为另一个程序的代码和数据,之后子进程就用该程序的数据执行该程序的代码,从而达到程序之间相互调用的效果。在学了C语言、C++或是JAVA等高级语言,你会知道,在这些语言中的函数是可以相互进行见调用的,但是在学习了Linux的前面的知识后,你就会有意无意的认识到其实进程也是与函数有相同之处的,进程之间也是可以相互调用的。程序之间相互调用带来的好处之一。那么下面就将这部分内容扩展。
29 0
|
5月前
|
人工智能 Java 程序员
一文彻底搞清楚C语言的函数
本文介绍C语言函数:函数是程序模块化的工具,由函数头和函数体组成,涵盖定义、调用、参数传递及声明等内容。值传递确保实参不受影响,函数声明增强代码可读性。君志所向,一往无前!
112 1
一文彻底搞清楚C语言的函数
|
6月前
|
算法 C语言
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
本文档介绍了如何编写两个子函数,分别求任意两个整数的最大公约数和最小公倍数。内容涵盖循环控制与跳转语句的使用、最大公约数的求法(包括辗转相除法和更相减损术),以及基于最大公约数求最小公倍数的方法。通过示例代码和测试说明,帮助读者理解和实现相关算法。最终提供了完整的通关代码及测试结果,确保编程任务的成功完成。
272 15
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
|
6月前
|
C语言
【C语言程序设计——函数】亲密数判定(头歌实践教学平台习题)【合集】
本文介绍了通过编程实现打印3000以内的全部亲密数的任务。主要内容包括: 1. **任务描述**:实现函数打印3000以内的全部亲密数。 2. **相关知识**: - 循环控制和跳转语句(for、while循环,break、continue语句)的使用。 - 亲密数的概念及历史背景。 - 判断亲密数的方法:计算数A的因子和存于B,再计算B的因子和存于sum,最后比较sum与A是否相等。 3. **编程要求**:根据提示在指定区域内补充代码。 4. **测试说明**:平台对代码进行测试,预期输出如220和284是一组亲密数。 5. **通关代码**:提供了完整的C语言代码实现
121 24
|
6月前
|
存储 C语言
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
311 16
|
6月前
|
存储 编译器 C语言
【C语言程序设计——函数】分数数列求和2(头歌实践教学平台习题)【合集】
函数首部:按照 C 语言语法,函数的定义首部表明这是一个自定义函数,函数名为fun,它接收一个整型参数n,用于指定要求阶乘的那个数,并且函数的返回值类型为float(在实际中如果阶乘结果数值较大,用float可能会有精度损失,也可以考虑使用double等更合适的数据类型,这里以float为例)。例如:// 函数体代码将放在这里函数体内部变量定义:在函数体中,首先需要定义一些变量来辅助完成阶乘的计算。比如需要定义一个变量(通常为float或double类型,这里假设用float。
168 3
|
6月前
|
存储 算法 安全
【C语言程序设计——函数】分数数列求和1(头歌实践教学平台习题)【合集】
if 语句是最基础的形式,当条件为真时执行其内部的语句块;switch 语句则适用于针对一个表达式的多个固定值进行判断,根据表达式的值与各个 case 后的常量值匹配情况,执行相应 case 分支下的语句,直到遇到 break 语句跳出 switch 结构,若没有匹配值则执行 default 分支(可选)。例如,在判断一个数是否大于 10 的场景中,条件表达式为 “num> 10”,这里的 “num” 是程序中的变量,通过比较其值与 10 的大小关系来确定条件的真假。常量的值必须是唯一的,且在同一个。
156 2
|
6月前
|
存储 编译器 C语言
【C语言程序设计——函数】回文数判定(头歌实践教学平台习题)【合集】
算术运算于 C 语言仿若精密 “齿轮组”,驱动着数值处理流程。编写函数求区间[100,500]中所有的回文数,要求每行打印10个数。根据提示在右侧编辑器Begin--End之间的区域内补充必要的代码。如果操作数是浮点数,在 C 语言中是不允许直接进行。的结果是 -1,因为 -7 除以 3 商为 -2,余数为 -1;注意:每一个数据输出格式为 printf("%4d", i);的结果是 1,因为 7 除以 -3 商为 -2,余数为 1。取余运算要求两个操作数必须是整数类型,包括。开始你的任务吧,祝你成功!
128 1

相关实验场景

更多