Linux下进程间的通信--信号

信号的概念:

在Linux操作系统中,信号是一种软件中断机制,用于通知进程某个事件已经发生。信号是Linux进程间通信(IPC)的一种简单且快速的方式,它可以用来处理各种异步事件,如用户输入、硬件事件或软件条件。

信号的特点:

进程可以选择阻塞某些信号,阻止这些信号的传递。

每个信号都有一个默认行为,例如终止进程、忽略或停止进程等

信号有不同的优先级,高优先级的信号会打断低优先级的信号处理。

信号可以跨越进程边界,由一个进程发送给另一个进程

信号机制是由操作系统内核实现的,与操作系统的调度和资源管理紧密相关。

信号的来源:

在Linux操作系统中,信号可以由多种来源产生,用于通知进程发生了某些重要事件。

  1. 硬件异常

    • SIGSEGV:尝试访问未分配的内存或无效的内存区域。
    • SIGFPE:发生浮点异常,如除以零。
    • SIGILL:执行了非法指令。
  2. 软件条件

    • SIGINT:通常由用户通过按下Ctrl+C产生,用于中断正在运行的程序。
    • SIGTERM:默认信号,用于请求程序自己终止。
    • SIGALRM:由alarm函数设置的计时器到期时产生。
  3. 用户干预

    • SIGKILL:由kill命令发送,用于立即终止程序,无法被捕获或忽略。
    • SIGSTOP:由kill命令发送,用于停止程序的执行,无法被捕获、忽略或由用户生成。
  4. 系统调用

    • SIGCHLD:子进程结束时,父进程会收到此信号。
    • SIGHUP:当控制终端关闭时,如关闭或拔出调制解调器,相关进程会收到此信号。
  5. 资源限制

    • SIGPIPE:写入一个没有读进程的管道时产生。
    • SIGXCPU:超过CPU时间限制。
    • SIGXFSZ:超过文件大小限制。
  6. I/O操作和设备状态变化

    • SIGURG:有紧急数据可从套接字读取时产生。
    • SIGIO:文件描述符上的I/O操作现在可进行时产生。
  7. 调度器事件

    • 某些实时调度器可能会在特定事件发生时发送信号。
  8. 外部设备或硬件设备

    • 特定硬件设备可能会在检测到特定事件时发送信号

信号的种类:

在Linux系统可以通过 kill -l 命令查看

 常用信号及其编号:

SIGHUP (1):挂起信号,通常在终端关闭时发送给前台进程组。

SIGINT (2):中断信号,通常由用户通过按下Ctrl+C产生。

SIGILL (4):非法指令信号,当进程执行非法指令时发送。

SIGABRT (6):中止信号,由abort()函数调用产生,用于异常终止进程。

SIGBUS (7):总线错误信号,当硬件异常,如内存访问错误时发送。

SIGFPE (8):浮点异常信号,如算术溢出、除以零等。

SIGKILL (9):杀死信号,用于立即终止进程,无法被捕获或忽略。

SIGUSR1 (10):用户定义信号1,用途由用户自定义。

SIGSEGV (11):段错误信号,当访问无效内存段时发送。

SIGUSR2 (12):用户定义信号2,用途由用户自定义。

SIGPIPE (13):管道信号,当写入一个没有读进程的管道时发送。

SIGTERM (15):终止信号,用于请求程序自己终止,可以被捕获或忽略。

SIGCHLD (17):子进程结束信号,当子进程结束时发送给父进程。

SIGCONT (18):继续信号,用于唤醒一个被停止的进程。

SIGSTOP (19):停止信号,用于停止进程的执行,无法被捕获或忽略。

SIGTTIN (21):后台进程试图读终端时发送。

SIGTTOU (22):后台进程试图写终端时发送。

SIGIO (29):I/O信号,当文件描述符上有可进行的I/O操作时发送。

信号处理的流程:

信号处理流程包含以下两个方面:

1. 信号的发送

信号的发送可以由以下几种方式触发:

  • 用户操作:用户可以通过键盘产生信号,如按下Ctrl+C通常发送SIGINT(中断信号)。
  • 软件生成:程序可以通过kill系统调用或raise函数发送信号给其他进程或自身。
  • 硬件异常:当程序执行非法操作(如除零、内存访问违规)时,硬件会触发信号,如SIGFPE(浮点异常)或SIGSEGV(段错误)。
  • 系统条件:系统在特定条件下也会发送信号,如SIGHUP(挂起信号)可能在控制终端关闭时发送。
  • 定时器超时:使用alarmsetitimertimer等定时器函数设置的定时器超时后,会发送如SIGALRMSIGVTALRM信号。

2. 信号的投递与处理

信号的投递与处理涉及以下几个步骤:

  • 信号队列:当一个信号被发送给进程时,它首先被放入进程的信号队列中。
  • 信号屏蔽:进程可以通过设置信号掩码(使用sigprocmask函数)来阻止某些信号的投递。被屏蔽的信号不会立即投递,直到它们被进程从屏蔽列表中移除。
  • 信号投递:内核负责将信号从队列中取出并投递给进程。如果信号未被屏蔽,内核会根据信号处理函数的设置来处理信号。
  • 默认处理:如果进程没有为信号设置处理函数,或者信号被忽略(使用signalsigaction函数设置为SIG_IGN),内核将执行信号的默认操作,如终止进程或忽略信号。
  • 用户定义的处理:如果进程为信号定义了处理函数,内核在信号到达时调用该函数。处理函数可以执行任何清理或响应操作。
  • 处理完成:信号处理函数执行完毕后,进程恢复到信号到达前的状态,继续执行。如果信号处理函数调用了如exit_Exit等函数,进程将终止。

在 Linux 中对信号的处理方式如下:

 在内核中的用于管理进程的结构为 task_struct ,

1.忽略信号

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

SIGKILL用于立即终止进程,而SIGSTOP用于停止进程的执行。这两个信号是为了保证系统管理员能够控制所有进程而设计的。

2.捕捉信号

  • 进程可以捕捉信号,并定义自己的信号处理函数。当信号发生时,如果进程为该信号注册了处理函数,内核会暂停进程的执行,转而执行该信号的处理函数。
  • 信号处理函数可以是标准函数,如signal()函数
signal()函数
函数头文件
#include <signal.h>

函数原型
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

函数参数
signum:指定要处理的信号的编号。例如,SIGINT(通常由Ctrl+C产生)。
handler:指向一个函数的指针,该函数将被调用以处理指定的信号。这个函数可以是程序自定义的,也可以是几个标准函数之一:
SIG_DFL:执行信号的默认操作。
SIG_IGN:忽略该信号

函数返回值
signal函数返回之前为该信号设置的处理函数的指针。如果之前没有设置处理函数,或者设置的是默认操作,它将返回SIG_DFL。如果之前设置的是忽略该信号,它将返回SIG_IGN。
如果signal函数调用失败,它将返回SIG_ERR,errno将被设置以指示错误原因
示例代码:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void signal_handler(int sig) {
    printf("Received signal %d\n", sig);
    // 执行必要的动作
}

int main() {
    // 设置SIGINT信号的处理函数
    signal(SIGINT, signal_handler);
    while(1) {
        sleep(1);
        printf("Looping...\n");
    }
    return 0;
}

3.执行默认操作

如果进程没有特别处理某个信号,Linux系统会为该信号执行默认操作。默认操作通常是终止进程、忽略信号或停止进程。

  • SIGTERM(信号15):默认操作是终止进程。
  • SIGCHLD(信号17):默认操作是忽略,通常用于子进程结束时通知父进程。
  • SIGCONT(信号18):默认操作是继续执行之前被停止的进程

信号发送操作

在Linux中,kill()raise() 函数是用于发送信号的两个常用方法。它们允许进程之间相互发送信号,或者进程可以向自己发送信号

1.kill()函数

kill() 函数可以向一个进程或进程组发送信号。这是最常用的信号发送方式,尤其是在需要向其他进程发送信号时。

​函数头文件
#include <sys/types.h>
#include <signal.h>

函数原型
int kill(pid_t pid, int sig);

函数参数
pid:目标进程的进程ID(PID)。
如果pid为正,则信号信号被发送到具有pid指定的ID的进程。
如果pid等于0,则向调用进程的进程组中的每个进程发送sig。
如果pid等于-1,则sig将发送到调用进程有权发送信号的每个进程,进程1 (init)除外
如果pid小于-1,则向进程组中ID为-pid的每个进程发送sig
sig:要发送的信号。可以是任何有效的信号,如SIGINT、SIGTERM等。

函数返回值
成功 返回0。
失败 返回-1,并设置errno以指示错误原因。

​

2.raise()函数

函数头文件
#include <sys/types.h>
#include <signal.h>

函数原型
int raise(int sig);

函数参数
sig:要发送给调用进程的信号编号。这是一个整数,表示特定的信号,如SIGINT、SIGTERM等。

函数返回值
成功 返回0。
失败 返回-1,并设置errno以指示错误原因

等待信号操作

在Linux系统中,pause() 函数用于使调用它的进程挂起,直到它接收到一个信号。这是一个简单的阻塞调用,常用于进程初始化后等待异步信号的情况

pause()函数

函数头文件
#include <unistd.h>

函数原型
int pause(void);

函数描述
pause() 函数使调用它的进程挂起,直到它捕获到一个信号。该进程将一直停留在pause()调用处,直到有信号到达并被处理。
该函数没有参数。
它返回时,通常意味着一个信号已经被接收并处理。

函数返回值
pause() 在信号处理函数执行后返回。如果信号处理函数返回,pause() 将返回。
如果因错误而失败,它将返回-1,并设置errno以指示错误原因

总结:

示例代码:

下面程序展示了如何使用信号和fork来创建和协调多个进程。父进程创建两个子进程,然后向它们发送不同的信号。第一个子进程等待并接收SIGUSR1信号,第二个子进程等待并接收SIGUSR2信号。子进程使用pause挂起等待信号,接收到信号后继续执行并退出。父进程等待子进程结束后也退出。这个程序展示了信号在进程间通信中的应用,并且正确地处理了子进程的结束,使用了wait(NULL)来避免子进程成为僵尸进程。

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

typedef void (*sighandler_t)(int);
void sighandler(int signum)
{
        printf("sighandle:%s\n",strsignal(signum));
}
int main()
{
        sighandler_t sig = signal(SIGUSR1,sighandler);
        if(sig==SIG_ERR)
        {
                perror("signal sig");
                exit(EXIT_FAILURE);
        }
        sighandler_t sigt  = signal(SIGUSR2,SIG_DFL);
        if(sigt==SIG_ERR)
        {
                perror("signal sigt");
                exit(EXIT_FAILURE);
        }
        pid_t pid = fork();
        if (pid == -1)
        {
                perror("fork failed");
                return 1;
        }
        else if(pid == 0)
        {
                printf("<1>chilb process pid :%d\n",getpid());
                pause();
                printf("<1>chilb process end\n");
                exit(EXIT_SUCCESS);
        }
        else
        {
                pid_t Apid = fork();
                if(Apid==-1)
                {
                        perror("fork1:");
                        exit(EXIT_FAILURE);
                }
                else if(Apid==0)
                {
                        printf("<2>chilb process Apid :%d\n",getpid());
                        pause();
                        printf("<2>chilb process end\n");
                        exit(EXIT_SUCCESS);
                }
                else
                {
                        printf("parent process pid :%d\n",getpid());
                        sleep(1);
                        int result = kill(pid,SIGUSR1);
                        if(result==-1)
                        {
                                perror("kill pid");
                                exit(EXIT_FAILURE);
                        }
                        int ret = kill(Apid,SIGUSR2);
                        if(ret==-1)
                        {
                                perror("kill Apid");
                                exit(EXIT_FAILURE);
                        }
                        printf("parent process end\n");
                }
        }
        wait(NULL);
        return 0;
}

代码解读:

定义sighandler_t类型,它是一个指向函数的指针,该函数接受一个整数参数(信号编号)并且没有返回值。

定义sighandler函数,它是一个信号处理函数,当接收到信号时被调用。它使用strsignal函数将信号编号转换为信号名称的字符串,并打印出来。

main函数中,首先设置SIGUSR1信号的处理函数为sighandler。如果设置失败,打印错误信息并退出。

SIGUSR2信号的处理函数设置为默认操作(SIG_DFL)。如果设置失败,打印错误信息并退出。

 创建第一个子进程。如果fork失败,打印错误信息并返回1。

 如果当前是子进程(pid == 0),打印子进程的PID,然后调用pause挂起等待信号。接收到信号后,打印结束信息并退出。

如果当前是父进程,再创建第二个子进程。如果fork失败,打印错误信息并退出。

如果当前是第二个子进程(Apid == 0),打印子进程的PID,然后调用pause挂起等待信号。接收到信号后,打印结束信息并退出。

如果当前是父进程,等待1秒,然后向第一个子进程发送SIGUSR1信号,向第二个子进程发送SIGUSR2信号。如果发送信号失败,打印错误信息并退出。

父进程打印结束信息。

父进程调用wait(NULL)等待任一子进程结束。

结语:

无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力

 

 

  • 15
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值