0
点赞
收藏
分享

微信扫一扫

Linux系统编程 [进程通信——信号]

小禹说财 2022-04-24 阅读 88
c++linux

文章目录

信号的概述

信号的特点

  • 简单
  • 不能携带大量信息
  • 满足某个特设条件才发送

在这里插入图片描述注意:这里信号的产生,注册,注销时信号的内部机制,而不是信号的函数实现。

信号编号

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

信号四要素

每个信号必备4要素,分别是:

可通过man 7 signal查看帮助文档获取:
在这里插入图片描述

Action为默认动作:

  • Term:终止进程
  • Ign: 忽略信号 (默认即时对该种信号忽略操作)
  • Core:终止进程,生成Core文件。(查验死亡原因,用于gdb调试)
  • Stop:停止(暂停)进程
  • Cont:继续运行进程

信号的状态

1) 产生
a) 当用户按某些终端键时,将产生信号。

b) 硬件异常将产生信号。

c) 软件异常将产生信号。

d) 调用系统函数(如:kill、raise、abort)将发送信号。

e) 运行 kill /killall命令将发送信号。

2) 未决状态:没有被处理

3) 递达状态:信号被处理了

阻塞信号集和未决信号集

信号的实现手段导致信号有很强的延时性,但对于用户来说,时间非常短,不易察觉。

1 阻塞信号集(信号屏蔽字)

2 未决信号集

信号产生函数

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

程序示例:

int main()
{
    pid_t pid = fork();
    if (pid == 0)
    {//子进程
        int i = 0;
        for (i = 0; i<5; i++)
        {
            printf("in son process\n");
            sleep(1);
        }
    }
    else
    {//父进程
        printf("in father process\n");
        sleep(2);
        printf("kill sub process now \n");
        kill(pid, SIGINT);
    }return 0;
}

raise函数

#include <signal.h>int raise(int sig);
功能:给当前进程发送指定信号(自己给自己发),等价于 kill(getpid(), sig)
参数:
    sig:信号编号
返回值:
    成功:0
    失败:非0

abort函数

#include <stdlib.h>void abort(void);
功能:给自己发送异常终止信号 6) SIGABRT,并产生core文件,等价于kill(getpid(), SIGABRT);
​
参数:无
​
返回值:无
​

alarm函数(闹钟)


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

测试程序:

int main()
{
    int seconds = 0;
​
    seconds = alarm(5);
    printf("seconds = %d\n", seconds);sleep(2);
    seconds = alarm(5);
    printf("seconds = %d\n", seconds);while (1);
    return 0;
}

setitimer函数(定时器)

#include <sys/time.h>int setitimer(int which,  const struct itimerval *new_value, struct itimerval *old_value);
功能:
    设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
参数:
    which:指定定时方式
        a) 自然定时:ITIMER_REAL → 14)SIGALRM计算自然时间
        b) 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM  只计算进程占用cpu的时间
        c) 运行时计时(用户 + 内核):ITIMER_PROF → 27)SIGPROF计算占用cpu及执行系统调用的时间
    new_value:struct itimerval, 负责设定timeout时间
        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
​
    old_value: 存放旧的timeout值,一般指定为NULL
返回值:
    成功:0
    失败:-1

示例程序:

void myfunc(int sig)
{
    printf("hello\n");
}int main()
{
    struct itimerval new_value;//定时周期
    new_value.it_interval.tv_sec = 1;
    new_value.it_interval.tv_usec = 0;//第一次触发的时间
    new_value.it_value.tv_sec = 2;
    new_value.it_value.tv_usec = 0;signal(SIGALRM, myfunc); //信号处理
    setitimer(ITIMER_REAL, &new_value, NULL); //定时器设置while (1);return 0;
}

信号集

信号集概述

在这里插入图片描述

自定义信号集函数

相关函数说明如下:

除sigismember外,其余操作函数中的set均为传出参数。sigset_t类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。

示例程序:


int main()
{
    sigset_t set;   // 定义一个信号集变量
    int ret = 0;sigemptyset(&set); // 清空信号集的内容// 判断 SIGINT 是否在信号集 set 里
    // 在返回 1, 不在返回 0
    ret = sigismember(&set, SIGINT);
    if (ret == 0)
    {
        printf("SIGINT is not a member of set \nret = %d\n", ret);
    }sigaddset(&set, SIGINT); // 把 SIGINT 添加到信号集 set
    sigaddset(&set, SIGQUIT);// 把 SIGQUIT 添加到信号集 set// 判断 SIGINT 是否在信号集 set 里
    // 在返回 1, 不在返回 0
    ret = sigismember(&set, SIGINT);
    if (ret == 1)
    {
        printf("SIGINT is a member of set \nret = %d\n", ret);
    }sigdelset(&set, SIGQUIT); // 把 SIGQUIT 从信号集 set 移除// 判断 SIGQUIT 是否在信号集 set 里
    // 在返回 1, 不在返回 0
    ret = sigismember(&set, SIGQUIT);
    if (ret == 0)
    {
        printf("SIGQUIT is not a member of set \nret = %d\n", ret);
    }return 0;
}

sigprocmask函数

#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:
    检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由 set 指定,而原先的信号阻塞集合由 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 不合法。

sigpending函数

#include <signal.h>int sigpending(sigset_t *set);
功能:读取当前进程的未决信号集
参数:
    set:未决信号集
返回值:
    成功:0
    失败:-1

示例程序:

int main()
{
    // 自定义信号集
    sigset_t myset, old;
    sigemptyset(&myset);// 清空 -》 0// 添加要阻塞的信号
    sigaddset(&myset, SIGINT);
    sigaddset(&myset, SIGQUIT);
    sigaddset(&myset, SIGKILL);// 自定义信号集设置到内核中的阻塞信号集
    sigprocmask(SIG_BLOCK, &myset, &old);
​
    sigset_t pend;
    int i = 0;
    while (1)
    {
        // 读内核中的未决信号集的状态
        sigpending(&pend);
        for (int i = 1; i<32; ++i)
        {
            if (sigismember(&pend, i))
            {
                printf("1");
            }
            else if (sigismember(&pend, i) == 0)
            {
                printf("0");
            }
        }
        printf("\n");
        sleep(1);
        i++;// 10s之后解除阻塞
        if (i > 10)
        {
            // sigprocmask(SIG_UNBLOCK, &myset, NULL);
            sigprocmask(SIG_SETMASK, &old, NULL);
        }
    }return 0;
}

信号捕捉

信号处理方式

【注意】:SIGKILL 和 SIGSTOP 不能更改信号的处理方式,因为它们向用户提供了一种使进程终止的可靠方法。

内核实现信号捕捉过程:

在这里插入图片描述

signal函数

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

示例程序:

// 信号处理函数
void signal_handler(int signo)
{
    if (signo == SIGINT)
    {
        printf("recv SIGINT\n");
    }
    else if (signo == SIGQUIT)
    {
        printf("recv SIGQUIT\n");
    }
}int main()
{
    printf("wait for SIGINT OR SIGQUIT\n");/* SIGINT: Ctrl+c ; SIGQUIT: Ctrl+\ */
    // 信号注册函数
    signal(SIGINT, signal_handler);
    signal(SIGQUIT, signal_handler);while (1); //不让程序结束return 0;
}

sigaction函数

#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
    检查或修改指定信号的设置(或同时执行这两种操作)。
​
参数:
    signum:要操作的信号。
    act:   要设置的对信号的新处理方式(传入参数)。
    oldact:原来对信号的处理方式(传出参数)。
​
    如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非空,则系统将此前指定信号的处理方式存入 oldact。
​
返回值:
    成功:0
    失败:-1

struct sigaction结构体:

1) sa_handler、sa_sigaction:信号处理函数指针,和 signal() 里的函数指针用法一样,应根据情况给sa_sigaction、sa_handler 两者之一赋值,其取值如下:

a) SIG_IGN:忽略该信号

b) SIG_DFL:执行系统默认动作

c) 处理函数名:自定义信号处理函数

2) sa_mask:信号阻塞集,在信号处理函数执行过程中,临时屏蔽指定的信号。

3) sa_flags:用于指定信号处理的行为,通常设置为0,表使用默认属性。它可以是一下值的“按位或”组合:

Ø SA_RESTART:使被信号打断的系统调用自动重新发起(已经废弃)

Ø SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。

Ø SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。

Ø SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。

Ø SA_RESETHAND:信号处理之后重新设置为默认的处理方式。

Ø SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。

信号处理函数:

示例程序:

void myfunc(int sig)
{
    printf("hello signal: %d\n", sig);
    sleep(5);
    printf("wake up .....\n");
}int main()
{
    // 注册信号捕捉函数
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = myfunc;
    // 设置临时屏蔽的信号
    sigemptyset(&act.sa_mask);  // 清空
    // ctrl + 反斜杠
    sigaddset(&act.sa_mask, SIGQUIT);sigaction(SIGINT, &act, NULL); //注册信号while (1);return 0;
}

sigqueue 函数(了解)

#include <signal.h>int sigqueue(pid_t pid, int sig, const union sigval value);
功能:
    给指定进程发送信号。
参数:
    pid : 进程号。
    sig : 信号的编号。
    value : 通过信号传递的参数。
        union sigval 类型如下:
            union sigval
            {
                int   sival_int;
                void *sival_ptr;
            };
返回值:
    成功:0
    失败:-1

发送信号示例代码如下:

/*******************************************************
*功能:     发 SIGINT 信号及信号携带的值给指定的进程
*参数:        argv[1]:进程号 argv[2]:待发送的值(默认为100)
*返回值:   0
********************************************************/
int main()
{
    if (argc >= 2)
    {
        pid_t pid, pid_self;
        union sigval tmp;
​
        pid = atoi(argv[1]); // 进程号
        if (argc >= 3)
        {
            tmp.sival_int = atoi(argv[2]);
        }
        else
        {
            tmp.sival_int = 100;
        }// 给进程 pid,发送 SIGINT 信号,并把 tmp 传递过去
        sigqueue(pid, SIGINT, tmp);
​
        pid_self = getpid(); // 进程号
        printf("pid = %d, pid_self = %d\n", pid, pid_self);
    }return 0;
}

接收信号示例代码如下:

// 信号处理回调函数
void signal_handler(int signum, siginfo_t *info, void *ptr)
{
    printf("signum = %d\n", signum); // 信号编号
    printf("info->si_pid = %d\n", info->si_pid); // 对方的进程号
    printf("info->si_sigval = %d\n", info->si_value.sival_int); // 对方传递过来的信息
}int main()
{
    struct sigaction act, oact;
​
    act.sa_sigaction = signal_handler; //指定信号处理回调函数
    sigemptyset(&act.sa_mask); // 阻塞集为空
    act.sa_flags = SA_SIGINFO; // 指定调用 signal_handler// 注册信号 SIGINT
    sigaction(SIGINT, &act, &oact);while (1)
    {
        printf("pid is %d\n", getpid()); // 进程号pause(); // 捕获信号,此函数会阻塞
    }return 0;
}

两个终端分别编译代码,一个进程接收,一个进程发送,运行结果如下:
在这里插入图片描述

不可重入、可重入函数

满足下列条件的函数多数是不可重入(不安全)的:

  • 函数体内使用了静态的数据结构;
  • 函数体内调用了malloc() 或者 free() 函数(谨慎使用堆);
  • 函数体内调用了标准 I/O 函数。

保证函数的可重入性的方法:

  • 在写函数时候尽量使用局部变量(例如寄存器、栈中的变量);
  • 对于要使用的全局变量要加以保护(如采取关中断、信号量等互斥方法),这样构成的函数就一定是一个可重入的函数。

Linux常见的可重入函数:
在这里插入图片描述
注意:信号处理函数应该为可重入函数

SIGCHLD信号

SIGCHLD信号产生的条件

如何避免僵尸进程

示例程序:


void sig_child(int signo)
{
    pid_t  pid;//处理僵尸进程, -1 代表等待任意一个子进程, WNOHANG代表不阻塞
    while ((pid = waitpid(-1, NULL, WNOHANG)) > 0)
    {
        printf("child %d terminated.\n", pid);
    }
}int main()
{
    pid_t pid;// 创建捕捉子进程退出信号
    // 只要子进程退出,触发SIGCHLD,自动调用sig_child()
    signal(SIGCHLD, sig_child);
​
    pid = fork();   // 创建进程
    if (pid < 0)
    { // 出错
        perror("fork error:");
        exit(1);
    }
    else if (pid == 0)
    { // 子进程
        printf("I am child process,pid id %d.I am exiting.\n", getpid());
        exit(0);
    }
    else if (pid > 0)
    { // 父进程
        sleep(2);   // 保证子进程先运行
        printf("I am father, i am exited\n\n");
        system("ps -ef | grep defunct"); // 查看有没有僵尸进程
    }return 0;
}

示例程序:

int main()
{
    pid_t pid;// 忽略子进程退出信号的信号
    // 那么子进程结束后,内核会回收, 并不再给父进程发送信号
    signal(SIGCHLD, SIG_IGN);
​
    pid = fork();   // 创建进程if (pid < 0)
    { // 出错
        perror("fork error:");
        exit(1);
    }
    else if (pid == 0)
    { // 子进程
        printf("I am child process,pid id %d.I am exiting.\n", getpid());
        exit(0);}
    else if (pid > 0)
    { // 父进程
        sleep(2);   // 保证子进程先运行
        printf("I am father, i am exited\n\n");
        system("ps -ef | grep defunct"); // 查看有没有僵尸进程
    }return 0;
}
举报

相关推荐

0 条评论