Linux - 进程间通信方式之信号

一、概述

1. 信号定义

信号是给程序提供一种可以处理异步事件的方法,它利用软件中断来实现。不能自定义信号,所有的信号都是系统预定义的。

二、信号的产生

1. 由shell终端根据当前发生的错误(段错误、非法指令等)和Ctrl+C而产生的信号。

例如:socket通信或管道通信,如果读端已经关闭,执行写操作(或发送数据)将导致执行写操作的进程收到SIGPIPE(表示管道破裂,该信号的默认行为是终止该进程)信号。

2. 在shell终端,使用kill或killall命令产生信号

示例1:

#include <unistd.h>

int main(int argc, char **argv) {
    while(1) {
        sleep(1);
    }

    return 0;
}

在终端1编译执行如上代码
在终端2shell下输入命令ps -ef | grep 示例1程序名 查询示例1程序的进程号
在终端2shell下输入命令kill -9 进程号
输入示例如下:

终端1:
gcc signal_kill.cc -o signal_kill.exe
./signal_kill.exe
终端2:
ps -ef | grep signal_kill.exe
kill -9 17601

3. 在代码中使用kill系统调用产生信号

  • 有哪些信号?
信号名称说明
SIGABORT程序异常终止
SIGALRM超时告警
SIGFPE浮点运算异常
SIGHUP连接挂断
SIGILL非法指令
SIGINT终端中断(Ctrl + C 产生该信号)
SIGKILL终止进程
SIGPIPE向没有读进程的管道写数据
SIGQUIT终端退出(Ctrl + \ 产生该信号)
SIGSEGV无效内存段访问
SIGTERM终止
SIGUSR1用户自定义信号1
SIGUSR2用户自定义信号2

以上信号如果不捕获,则进程接收到后都会终止!

信号名称说明
SIGCHLD子进程已停止或退出
SIGCONT让暂停的进程继续执行
SIGSTOP停止执行(即暂停)
SIGTSTP中断挂起
SIGTTIN后台进程尝试读操作
SIGTTOU后台进程尝试写操作

三、信号的处理

  1. 忽略信号
  2. 捕捉信号,指定信号处理函数进行处理
  3. 执行系统默认动作,大多数都是终止进程
    示例代码 - 忽略和捕获信号:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void process_USR2(int signum) {
    printf("接收到信号SIGUSR2:%d\n", signum);
}

int main(int argc, char *argv[]) {
    // 忽略信号SIGUSR1
    signal(SIGUSR1, SIG_IGN);

    // 捕获信号SIGUSR2,并处理
    signal(SIGUSR2, process_USR2);

    while(1) {
        sleep(1);
    }

    return 0;
}
/***********************************************************************
 * 在终端1下编译执行
 * 在终端2下使用kill命令发送相关信号
************************************************************************/

四、信号的捕获

1. 含义:指定接受到某种信号后,去执行指定的函数。

:SIGKILL和SIGSTOP不能被捕获;即这两种信号的响应动作不能被改变。

2. 信号的安装

安装信号有两种方式,分别是:signal和sigaction。在实战中推荐使用sigaction。

  1. 使用signal
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    signal的参数2可取以下特殊值:
取值含义
SIG_IGN忽略信号
SIG_DFL恢复默认行为

:signal的返回类型和第二个参数都是函数指针类型
:使用SIG_DFL时,仅当第一次调用自定义的行为后马上使用SIG_DFL就可恢复,如果连续捕获多次后,就不确定。

示例代码 - 改变终端中断信号的行为:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void process_INT(int signum) {
    printf("接收到中断信号:%d\n", signum);
}

int main(int argc, char *argv[]) {
    signal(SIGINT, process_INT);

    while(1) {
        sleep(1);
    }

    return 0;
}
/***********************************************************************
 * 1.编译运行
 * 2.按下Ctrl + C 会输出如下内容:^C接收到中断信号:2
 * 3.结束当前进程:开启另一个终端,使用ps命令获取进程号,通过kill -9 杀死进程
 * 命令示例:
 * ps -er | grep ./a.out
 * kill -9 1011
************************************************************************/

示例代码 - 恢复信号的默认行为:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void process_INT(int signum) {
    printf("Catch signal: %d\n", signum);

    signal(SIGINT, SIG_DFL);    // 等同于 signal(signum, SIG_DFL);
}

int main(int argc, char *argv[]) {
    signal(SIGINT, process_INT);

    while(1) {
        sleep(1);
    }

    return 0;
}
/***********************************************************************
 * 1. 编译执行
 * 2. 第一次按下Ctrl + C 输出如下内容:^CCatch signal: 2
 * 3. 第二次按下Ctrl + C 输出如下内容并退出程序:^C
************************************************************************/

:使用SIG_DFL时,仅当第一次调用自定义的行为后马上使用SIG_DFL就可恢复,如果连续捕获多次后,就不确定。

  1. 使用sigaction
// 头文件
#include <signal.h>

// 原型
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

// struct sigaction
struct sigaction {
	void     (*sa_handler)(int);	// 信号的响应函数
    sigset_t sa_mask;				// 屏蔽信号集
    int      sa_flags;				// 当sa_flags中包含 SA_RESETHAND时,接受到该信号并调用指定的信号处理函数执行之后,把该信号的响应行为重置为默认行为SIG_DFL
    ...
};

补充:
当sa_mask包含某个信号A时,则在信号处理函数执行期间,如果捕获到该信号A,则阻塞该信号A(即暂时不响应该信号),知道信号处理函数执行结束。即信号处理函数执行完之后,再响应该信号A。

示例代码 - sigaction函数的使用:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_response(int signum) {
    printf("Catch a signal: %d\n", signum);
}

int main() {
    struct sigaction action;
    action.sa_handler = signal_response;
    // sigemptyset(sigset_t *set) - 将set给出的信号集初始化为空,并将所有信号排除在该集之外。
    sigemptyset(&action.sa_mask);
    action.sa_flags = 0;

    sigaction(SIGINT, &action, 0);

    while(1) {
        sleep(1);
    }

    return 0;
}

示例代码 - 使用函数sigaction()实现只执行一次自定义信号:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_once_restore(int signum) {
    printf("Catch a signal: %d\n", signum);
}

int main(int argc, char *argv[]) {
    struct sigaction action;

    action.sa_handler = signal_once_restore;
    sigemptyset(&action.sa_mask);
    action.sa_flags = SA_RESETHAND;

    sigaction(SIGINT, &action, 0);

    while(1) {
        sleep(1);
    }

    return 0;
}
  1. signal 和 sigaction的区别:sigaction比signal更"健壮"。

五、信号的发送

1. 信号的发送方式

a. 在shell终端使用快捷键产生信号(例如:Ctrl + C)
b. 在shell终端使用kill,killall命令
c. 在代码中使用kill函数和alarm函数

2. 使用kill函数

/****************************************************************************
 * 函数:int kill(pid_t pid, int sig)
 * 功能:给指定的进程发送信号
 * 用法:man 2 kill
 * 参数:
 * 		pid	-	向pid发送信号
 * 				pid > 0:信号sig被发送到具有PID指定的ID的进程。
 * 				pid == 0:将sig发送到调用进程的进程组中的每个进程。
 * 				pid == -1:将sig发送到调用进程有权为其发送信号的每个进程(进程1(Init)除外),但请参见下面的内容。
 * 				pid < -1:将sig发送到进程组中ID为-pid的每个进程。
 * 		sig -	要发送的信号。
 * 				sig == 0:则不会发送信号,但仍会执行存在和权限检查;这可用于检查是否存在允许调用者发送信号的进程ID或进程组ID。
 * 返回:
 * 		成功:返回0
 * 		失败:返回-1,并设置errno
 * 说明: 给指定的程序发送信号需要"权限":普通用户的进程只能给该用户的其他进程发送信号,root用户可以给所有用户的进程发送信号。
*****************************************************************************/

示例程序 - 默认输出"child process work!",输入A后转大写,输入a后转小写:

#include <stdio.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

int flags = 0;

void signal_USR1(int signum) {
    flags = 1;
}

void signal_USR2(int signum) {
    flags = 0;
}

int main(int argc, char *argv[]) {
    int pid = fork();

    if(pid < 0) {
        printf("fork() - failed! reason: %s\n", strerror(errno));
    } else if(pid == 0) {

            const char *msg = NULL;
            struct sigaction act;
            act.sa_handler = signal_USR1;
            sigemptyset(&act.sa_mask);
            act.sa_flags = 0;
            sigaction(SIGUSR1, &act, 0);

            act.sa_handler = signal_USR2;
            sigaction(SIGUSR2, &act, 0);

            while(1) {
                if(flags) {
                    msg = "CHILD PROCESS WORK!";
                } else {
                    msg = "child process work!";
                }
                printf("%s\n", msg);
                sleep(1);
            }
    } else {
        while(1) {
            char c = getchar();
            if(c == 'A') {
                kill(pid, SIGUSR1);
            } else if(c == 'a') {
                kill(pid, SIGUSR2);
            }
        }
    }
    return 0;
}

示例程序 - 闹钟:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>

int alarm_flags = 0;

void signal_alarm(int signum) {
    alarm_flags = 1;
}

int main(int argc, char *argv[]) {
    int pid = fork();

    if(pid < 0) {
        fprintf(stderr, "fock() - failed! reason: %s\n", strerror(errno));
        return -1;
    } else if(pid == 0) {
        sleep(5);
        // 向父进程发送闹钟信号
        // getppid() - 获取父进程id
        kill(getppid(), SIGALRM);
    } else {
        struct sigaction action;
        action.sa_handler = signal_alarm;
        sigemptyset(&action.sa_mask);
        action.sa_flags = 0;

        sigaction(SIGALRM, &action, NULL);

        // 暂时挂起该进程,直到收到任意信号
        pause();

        if(alarm_flags == 1) {
            printf("The parent process has been awakened!\n");
        }
    }
    return 0;
}

3. 使用alarm函数

/****************************************************************************
 * 函数:unsigned int alarm(unsigned int seconds);
 * 功能:在等待seconds秒时间之后给该进程本身发送一个SIGALRM信号
 * 用法:man 2 alarm
 * 参数:
 * 		seconds -	等待的秒数
 * 					seconds == 0:则取消所有挂起的alarm
 * 返回:
 *      失败:返回-1
 * 		成功:返回上次闹钟的使用时间(单位:秒)
 * 注意:时间的单位是"秒"
 * 		实际闹钟时间比指定的时间要大一些
 * 		如果参数为0,则取消已设置的闹钟
 * 		如果闹钟时间还没到,再次调用alarm,则重新开始计时
 * 		每个进程最多只能使用1个闹钟
*****************************************************************************/

示例代码 - 闹钟:

#include <time.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

int alarm_flags = 0;

void signal_alarm(int signum) {
    alarm_flags = 1;
}

int main() {
    struct sigaction action;
    action.sa_handler = signal_alarm;
    action.sa_flags = 0;
    sigemptyset(&action.sa_mask);
    sigaction(SIGALRM, &action, NULL);

    printf("time = %ld\n", time((time_t*)0));

    int ret = alarm(5);
    if(ret == -1) {
        printf("alarm() - error!\n");
        return -1;
    }

    // 挂起当前进程,直到收到任意信号
    pause();

    if(alarm_flags) {
        printf("The process is awakened, time = %ld\n", time((time_t*)0));
    }

    return 0;
}

4. 使用raise

/****************************************************************************
 * 函数:int raise(int sig);
 * 功能:给本进程自身发送信号。
 * 参数:sig - 需要发送的信号
 * 返回:
 * 		成功 - 返回0
 * 		失败 - 返回非0值
*****************************************************************************/

六、发送多个信号

  1. 某进程正在执行某个信号对应的操作函数期间(该信号的安装函数),如果此时,该进程又多次收到同一个信号(同一种信号值的信号),则:
    如果该信号是不可靠信号(<32),则只响应一次;
    如果该信号是可靠信号(>32),则可以响应多次(不会遗漏),但是都必须要等该次响应函数执行完成之后才能响应下一次。
  2. 某进程正在执行某个信号对应的操作函数期间(该信号的安装函数),如果此时,该进程收到另一个信号(不同信号值的信号),则:
    如果该信号被包含在当前信号的signaction的sa_mask(信号屏蔽集)中,则不会立即处理该信号。直到当前的信号处理函数执行完之后,才去执行该信号的处理函数。
    否则:立即中断当前执行过程(如果处于睡眠,比如sleep, 则立即被唤醒)而去执行这个新的信号响应。新的响应执行完之后,再在返回至原来的信号处理函数继续执行。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void myhandle(int sig)
{
        printf("Catch a signal : %d\n", sig);
        int i;
        for (i=0; i<10; i++) {
                sleep(1);
}
        printf("Catch end.%d\n", sig);
}

int main(void)
{
        struct sigaction act, act2;

        act.sa_handler = myhandle;
        sigemptyset(&act.sa_mask);
        sigaddset(&act.sa_mask, SIGUSR1);
       act.sa_flags = 0;

        sigaction(SIGINT, &act, 0);


        act2.sa_handler = myhandle;
        sigemptyset(&act2.sa_mask);
        act2.sa_flags = 0;
        sigaction(SIGUSR1, &act, 0);

        while (1) {

        }

        return 0;
}

七、信号集

1. 概念:

信号集:用sigset_t(unsigned long int)类型表示。用来表示包含多个信号的集合。

2. 信号集的基本操作

函数功能
sigemptyset把信号集清空
sigfillset把所有已定义的信号填充到指定的信号集
sigdelset从指定的信号集中删除指定的信号
sigaddset从指定的信号集中添加指定的信号
sigismember判断指定信号是否在指定的信号集中。如果在,返回1;如果不在,返回0;信号无效,返回-1

3. 进程的"信号屏蔽字"

进程的"信号屏蔽字"是一个信号集
向目标进程发送某个信号时,如果这个信号在目标进程的信号屏蔽集中,则目标进程将不会响应该信号(即不会执行该信号的信号处理函数)。如果进程的信号屏蔽字中不再包含该信号时,则会立即响应这个信号(执行对应的函数,在其他进程向此进程发送过该信号的情况下)。
/****************************************************************************
 * 函数:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
 * 功能:修改进程的“信号屏蔽字”
 * 参数:
 * 		how - 取值如下:
 * 				SIG_BLOCK	- 把参数set中的信号添加到信号屏蔽字中
 * 				SIG_UNBLOCK	- 把参数set中的信号从信号屏蔽字中删除
 * 				SIG_SETMASK	- 把参数set中的信号设置为信号屏蔽字
 * 		set - 信号屏蔽字
 * 		oldset - 之前的信号屏蔽字
 * 返回:
 * 		成功:返回0
 * 		失败:返回-1,并设置errno
*****************************************************************************/

示例代码 - 将信号SIGINT加入/移出信号屏蔽集:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_INT(int signum) {
    printf("Catch a signal: %d\n", signum);
}

int main(int atgc, char *argv[]) {
    struct sigaction action;
    action.sa_handler = signal_INT;
    sigemptyset(&action.sa_mask);
    action.sa_flags = 0;
    sigaction(SIGINT, &action, NULL);

    sigset_t proc_sig_msk, old_mask;
    sigemptyset(&proc_sig_msk);
    sigaddset(&proc_sig_msk, SIGINT);

    // 将proc_sig_msk添加到原信号屏蔽集中
    sigprocmask(SIG_BLOCK, &proc_sig_msk, &old_mask);
    printf("Blocked signal SIGINT.\n");
    sleep(5);
    printf("Unblocking signal SIGINT.\n");
    // 将prorc_sig_msk从原信号屏蔽集中移除
    sigprocmask(SIG_UNBLOCK, &proc_sig_msk, &old_mask);
    printf("Unblocked signal SIGINT.\n");

    while(1) {
        sleep(1);  // 休眠1秒
    }

    return 0;
}

4. 获取未处理的信号

当进程的信号屏蔽字中信号发生时,这些信号不会被该进程响应,可通过sigpending函数获取这些已经发生了但是没有被处理的信号

/****************************************************************************
 * 函数:int sigpending(sigset_t *set);
 * 功能:返回等待传递给调用线程的一组信号(即在阻塞时引发的信号)。
 * 参数:
 * 		set - 用于接收信号
 * 返回:
 * 		成功 - 返回0
 * 		失败 - 返回-1,并设置errno
*****************************************************************************/

5. 阻塞式等待信号

  1. pause:阻塞进程,直到发生任意信号
  2. sigsuspend:用指定的参数设置信号屏蔽字,然后阻塞时等待信号的发生。即等待信号屏蔽字之外的信号
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值