LInux高级系统编程-4 信号

进程间通讯

如何将 A 进程中的数据传入 B 进程呢 ?
我们要使用进程间通讯
中文名 : 进程间通讯
英文名 :IPC
英文全称 :Inter Processes Communication
作用 :
        数据传输:一个进程需要将它的数据发送给另一个进程。
        资源共享:多个进程之间共享同样的资源。
        通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件。
        进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进 程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变。
发展史 :
        最初的 UNIX 进程间通信
        SYSTEM V 进程间通信
        POSIX 进程间通信 (POSIX:Portable Operating System interface 可移植操作系 统接口)
        Socket 进程间通信
        Linux 把优势都继承了下来并形成了自己的 IPC
Linux 进程间通信机制
LInux高级系统编程-4 信号_第1张图片
分类:
        信号 (Semaphore)
        管道(pipe)
        消息队列 (Message Queue)
        内存共享 (Shared Memory)
        套接字(socket)

信号

概念:

信号是 Linux 进程间通信的最古老的方式。
特点 :
        简单
        不能携带大量信息
        满足某个特设条件才发出。
注意 :
        信号是一种异步通信方式。
        信号是软件中断,它是在软件层次上对中断机制的一种模拟。
        信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某 一个突发事件。
        进程不必等待信号的到达,进程也不知道信号什么时候到达。
        信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通 知用户空间进程发生了哪些系统事件。
名词解释:
        同步: 事件、操作或进程是有序的 , 一个操作必须在另一个操作完成后开始执行。
        异步: 事件、操作或进程是独立的 , 可以在不等待其他操作完成的情况下开始执行
        中断机制: 是现代计算机系统中的基本机制之一,完成了对计算机各个事件(如时钟、 键盘等)响应工作
        硬件中断: 是由硬件设备触发的中断,如时钟中断、串口接收中断、外部中断等。当硬 件设备有数据或事件需要处理时,会向CPU 发送一个中断请求, CPU 在收到中断请求后, 会立即暂停当前正在执行的任务,进入中断处理程序中处理中断请求。硬件中断具有实时性强、可靠性高、处理速度快等特点。
        软件中断: 是由软件程序触发的中断,如系统调用、软中断、异常等。软件中断不是由 硬件设备触发的,而是由软件程序主动发起的,一般用于系统调用、进程切换、异常处理等任务。软件中断需要在程序中进行调用,其响应速度和实时性相对较差,但是具有灵活性和可控性高的特点。

完整的信号周期

信号的注册
信号的产生
信号的处理函数(默认、忽略、自定义)
信号的注销
注意:这里信号的产生,注册,注销是信号的内部机制,而不是信号的函数实现。

信号的编号

每个信号的名字都以字符 SIG 开头。
每个信号和一个数字编码相对应,在头文件 signum.h 中,这些信号都被定义为正整 数。
信号名定义路径: /usr/include/i386-linux-gnu/bits/signum.h
Linux 下,要想查看这些信号和编码的对应关系,可使用命令: kill -l
不存在编号为 0 的信号。
1-31 号信号称之为常规信号 ( 也叫普通信号 , 标准信号 )
34-64 号信号称之为实时信号 ( 自定义信号 )
通过 man 7 signal 查看信号帮助文档
注意 :9) SIGKILL 19) SIGSTOP 信号,不允许忽略和捕捉,只能执行默认动作。
(重要)
SIGQUIT 信号由 :Ctrl + \ 触发
LInux高级系统编程-4 信号_第2张图片
LInux高级系统编程-4 信号_第3张图片
LInux高级系统编程-4 信号_第4张图片
LInux高级系统编程-4 信号_第5张图片
LInux高级系统编程-4 信号_第6张图片

信号的产生

        1, 当用户按某些终端键时,将产生信号。 终端上按 “Ctrl+c” 组合键通常产生中断信 号 SIGINT 终端上按 “Ctrl+\” 键通常产生中断信号 SIGQUIT 终端上按 “Ctrl+z” 键通 常产生中断信号 SIGSTOP 等。
        2,硬件异常将产生信号。 除数为 0 ,无效的内存访问等。这些情况通常由硬件检测到, 并通知内核,然后内核产生适当的信号发送给相应的进程。
        3,软件异常将产生信号。 当检测到某种软件条件已发生 ( 如:定时器 alarm) ,并将其 通知有关进程时,产生信号。
        4,调用系统函数 ( 如: kill raise abort) 将发送信号。
注意:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。
        5,运行 kill/killall 命令将发送信号。 此程序实际上是使用 kill 函数来发送信 号。也常用此命令终止一个失控的后台进程。

kill函数

作用:

给指定进程发送指定信号(不一定是杀死)

语法:

头文件:

        #include

        #include

函数:

        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

#include 
#include 
#include 
#include 
int main(int argc, char const *argv[])
{
    pid_t pid = fork();
    if(pid == 0){
        while (1)
        {
            printf("找打、\n");
            sleep(1);
        }
        
    }else if(pid > 0){
        printf("给你三秒、\n");
        sleep(3);
        kill(pid,SIGKILL);
    }
    return 0;
}

raise函数(自杀)

作用 : 给当前进程发送指定信号 ( 自己给自己发 ) ,等价于 kill(getpid(), sig), 自杀
语法:
所需头
        #include
函数
        int raise(int sig);
参数 :
        sig:信号编号
返回值:
        成功:0
        失败:非 0
#include 
#include 
#include 
int main(int argc, char const *argv[])
{
    int i = 0;
    while (1)
    {
        sleep(1);
        printf("好无聊哦,%d\n", i);
        if (i == 3)
        {
            printf("死给你看\n");
            raise(SIGKILL);
        }
        i++;
    }
    return 0;
}

abort函数(自杀)

作用 : 给自己发送异常终止信号 6) SIGABRT ,并产生 core 文件,等价于 kill(getpid(), SIGABRT)
core 文件 ( 了解 )
        core就是内核
        core文件会包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各种 函数调用堆栈信息等,我们可以理解为是程序工作当前状态存储生成第一个文件,许多 的程序出错的时候都会产生一个core 文件,通过工具分析这个文件,我们可以定位到程 序异常退出的时候对应的堆栈调用等信息,找出问题所在并进行及时解决。
#include 
#include 
#include 
#include 
#include 
int main(int argc, char const *argv[])
{
    /*
        指针数组,存储指针的数组
        本质上是一个数组,该数组中存储的是指针
        定义语法:数据类型 *指针变量名[长度];
        数组指针
        本质上是个指针
        定义语法:指向的数组的类型(*指针变量名)[指向的数组的元素个数]
    */
    char *myinfos[3] = {"我要发信号了", "我真的要发信号了", "拜拜咯,下次见"};
    int i = 0;
    while (1)
    {
        char * info = myinfos[i];
        printf("%s\n",info);
        sleep(1);
        i++;
        if(i == 3){
            abort();
        }
    }
    
    return 0;
}

alarm函数(自杀)

作用:

        设置定时器(闹钟),在指定时间后(单位:秒),内核会给当前进程发送SIGALRM信号,默认动作终止进程,每个进程都有且只有一个定时器。

        取消定时器alarm(0);返回旧闹钟余下的秒数

头文件:

        #include
函数
        unsigned int alarm(unsigned int seconds);
参数 :
        seconds:时间 , 单位秒
返回值 :
        若以前没有设置过定时器,或设置的定时器已超时,返回0
        否则返回定时器剩余的秒数,并重新设定定时器。
注意:定时,与进程状态无关 ( 即自然定时法 ) !就绪、运行、挂起 ( 阻塞、暂停 ) 、终止、僵 尸…… 无论进程处于何种状态, alarm 都计时。
不阻塞当前进程
#include 
#include 
#include 
#include 
int main(int argc, char const *argv[])
{
    int tim = 0;
    tim = alarm(5);
    printf("倒计时  %d\n",tim);
    sleep(1);
    tim = alarm(5);
    printf("倒计时  %d\n",tim);
    while (1)
    {
        /* code */
    }
    
    return 0;
}

setitimer函数(定时器)[扩展]

设置定时器 ( 闹钟 ) 。 可代替 alarm 函数。精度微秒 us ,可以实现周期定时。
所需头文件
        #include
函数
        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 结构体的指针,用于指定新的定时器值 (启动时间和间隔时间)。
        old_value:一个指向 struct itimerval 结构体的指针,用于获取旧的定时器 值,即之前设置的定时器值。如果不需要获取旧的定时器值,则可以传入NULL
LInux高级系统编程-4 信号_第7张图片
struct itimerval 结构体
struct itimerval {
        struct timerval it_interval; // 间隔时间
        struct timerval it_value; // 延迟时间
};
struct timerval结构
struct timeval {
        long tv_sec; // 秒
        long tv_usec; // 微秒
};
#include 
#include 
#include 
void fun()
{
    printf("已处理\n");
}
int main(int argc, char const *argv[])
{
    struct itimerval new_v;
    new_v.it_value.tv_sec = 5;
    new_v.it_value.tv_usec = 0;
    new_v.it_interval.tv_sec = 1;
    new_v.it_interval.tv_usec = 0;
    signal(SIGALRM, fun);                 // 信号处理
    setitimer(ITIMER_REAL, &new_v, NULL); // 定时器设置
    while (1)
        ;
    return 0;
}

pause函数

将调用进程挂起直至捕捉到信号为止。这个函数通常用于判断信号是否已到
#include
int pause(void)
返回值 : 直到捕获到信号, pause 函数才返回 -1 ,且 errno 被设置成 EINTR
#include 
#include 
#include 
void fun()
{
    printf("已处理\n");
}
int main(int argc, char const *argv[])
{
    printf("进程以开始,当前进程:%d\n", getpid());
    signal(SIGALRM, fun);
    alarm(3);
    int x = pause();
    printf("进程结束%d\n", x);
    return 0;
}

信号处理

        一个进程收到一个信号的时候,可以用如下方法进行处理:
        1) SIG_DFL 执行系统默认动作 对大多数信号来说,系统默认动作是用来终止该进程。
        2) SIG_IGN 忽略此信号 ( 丢弃 ) 接收到此信号后没有任何动作。
        3)信号处理函数名 : 执行自定义信号处理函数 ( 捕获 ) 用用户定义的信号处理函数处理 该信号。
【注意】: SIGKILL SIGSTOP 不能更改信号的处理方式,因为它们向用户提供了一种 使进程终止的可靠方法
捕捉信号并且信号信号的处理方式有两个函数 ,signal sigaction

signal函数

        注册信号处理函数(不可用于 SIGKILL SIGSTOP 信号),即确定收到信号后处理函 数的入口地址。此函数不会阻塞
所需头文件
        #include
函数 :
        //定义的一个函数指针
        typedef void(*sighandler_t)(int);
        //signal函数
        sighandler_t signal(int signum, sighandler_t handler);
参数 :
        signum:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过 命令 kill - l("l" 为字母 ) 进行相应查看。
        handler : 取值有 3 种情况:
        SIG_IGN:忽略该信号
        SIG_DFL:执行系统默认动作
        信号处理函数名:自定义信号处理函数
返回值 :
        成功:第一次返回 NULL ,下一次返回此信号上一次注册的信号处理函数的地址。 如果需要使用此返回值,必须在前面先声明此函数指针的类型。
        失败:返回 SIG_ERR
示例 : 自定义 ctrl+c 信号处理1
#include 
#include 
#include 
void myfun(int sigcode)
{
    printf("自定义函数\n");
    _exit(-1);
}
int main(int argc, char const *argv[])
{
    // ctrl+c发出的信息为SIGINT
    signal(SIGINT, myfun);
    while (1)
        ;
    return 0;
}
示例 : 自定义 ctrl+c 信号处理 2
#include 
#include 
#include 
#include 
#include 
char *p;
void myfun(int sigcode)
{
    if (p != NULL)
    {
        free(p);
    }
    printf("p的空间被释放了\n");
    _exit(-1);
}
int main(int argc, char const *argv[])
{
    p = malloc(50);
    strcpy(p, "helloSIG");
    signal(SIGINT, myfun);
    while (1)
    {
        printf("%s\n", p);
        sleep(1);
    }
    return 0;
}

注意:

        signal函数由 ANSI 定义 , 由于历史原因在不同版本的 Unix 和不同版本的 Linux 中 可能有不同的行为。因此应该尽量避免使用它, 取而代之使用 sigaction 函数

sigaction函数

作用 : 检查或修改指定信号的设置(或同时执行这两种操作)。
函数
所需头文件 :
        #include
函数 :
        int sigaction(int signum, const struct sigaction *act, struct sigaction* oldact);
参数 :
        signum:要操作的信号。
        act: 要设置的对信号的新处理方式(传入参数)。
        oldact:原来对信号的处理方式(传出参数)。
注意 :
        如果act 指针非空 , 则要改变指定信号的处理方式 ( 设置 ), 如果 oldact 指针非空 , 则 系统将此前指定信号的处理方式存入oldact
返回值 :
        成功:0
        失败:-1
struct sigaction :结构体
struct sigaction
{
    void (*sa_handler)(int);                        // 旧的信号处理函数指针
    void (*sa_sigaction)(int, siginfo_t *, void *); // 新的信号处理函数指针
    sigset_t sa_mask;                               // 信号阻塞集
    int sa_flags;                                   // 信号处理的方式
    void (*sa_restorer)(void);                      // 已弃用
};
/*
1,sa_handler,sa_sigaction:信号处理函数指针,和signal()里的函数指针用法
一样,应根据情况给 sa_sigaction、sa_handler 两者之一赋值,其取值如下:
a SIGIGN:忽略该信号
b SIGDFL:执行系统默认动作
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(*sa_sigaction)(int signum, siginfo_t *info, void *context);
参数说明:
signum:信号的编号。
info:记录信号发送进程信息的结构体。
context:可以赋给指向ucontext_t类型的一个对象的指针,以引用在传递信
号时被中断的接收进程或线程的上下文。
*/

示例:

#include 
#include 
#include 
#include 
#include 
void myfunc(int sigcode)
{
    printf("ctrl+c的信号编码是:%d\n", sigcode);
    _exit(-1);
}
int main(int argc, char const *argv[])
{
    struct sigaction act;
    act.sa_handler = myfunc;
    sigaction(SIGINT, &act, NULL);
    while (1)
        ;
    return 0;
}
示例 2: 发出定义信号
#include 
#include 
#include 
#include 
#include 
#include 
void myfunc(int sigcode)
{
    printf("SIGUSR1信号编号是:%d\n", sigcode);
    _exit(-1);
}
int main(int argc, char const *argv[])
{
    // 注意vscode中变量名会报错,不用管
    struct sigaction act;
    act.sa_handler = myfunc;
    // 注册监听的信号
    sigaction(SIGUSR1, &act, NULL);
    // 发出自定义信号
    kill(getpid(), SIGUSR1);
    while (1)
        ;
    return 0;
}

可重入函数

函数可以同时被很多进程调用,并且保证每个模块都能得到期望的结果
1 、不使用或返回 静态的数据、全局变量(除非用信号量互斥)。
2 、不调用动态内存分配、释放的函数。
3 、不调用任何不可重入的函数(如标准 I/O 函数)
常见的可重入函数列表
LInux高级系统编程-4 信号_第8张图片
#include 
#include 
#include 
#include 
#include 
void show(int pid)
{
    for (int i = 0; i < 3; i++)
    {
        char buf[50] = {0};
        sprintf(buf, "进程%d第%d次执行%s", pid, i, "\r\n");
        write(1, buf, sizeof(buf));
        sleep(1);
    }
}
int main(int argc, char const *argv[])
{
    for (int i = 0; i < 5; i++)
    {
        int pid = fork();
        if (pid == 0)
        {
            show(getpid());
            _exit(0);
        }
        else if (pid > 0)
        {
            printf("子进程%d被创建了\n", pid);
        }
    }
    while (1)
    {
        int id = waitpid(-1, NULL, WNOHANG);
        if (id > 0)
        {
            printf("进程%d被回收\n", id);
        }
        else if (id == -1)
        {
            break;
        }
    }
    return 0;
}

信号集

        在PCB 中有两个非常重要的信号集。一个称之为 阻塞信号集 ,另一个称之为 未决信号 集” 。 这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对其 进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信 号集进行修改
        信号阻塞集:将某些信号加入 集合,对他们设置屏蔽,当屏蔽 x 信号后,再收到该信 号,该信号的处理将暂缓。
        未决信号集:未被处理的信号集合。
LInux高级系统编程-4 信号_第9张图片

自定义信号集函数

所需头文件
        #include
函数
        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); //判断信号是否存在于 集合中
#include 
#include 
#include 
#include 
#include 
int main(int argc, char const *argv[])
{
    // 定义一个信号集
    sigset_t set;
    // 清空set集
    sigemptyset(&set);
    // 将SIGINT添加到set集合中
    sigaddset(&set, SIGINT);
    // 将SIGTSTP添加到set集和中
    sigaddset(&set, SIGTSTP);
    if (sigismember(&set, SIGINT))
    {
        printf("SIGINT是在set集合中\n");
    }
    else
    {
        printf("SIGINT不在set集合中\n");
    }
    // 将SIGINT从集合中删除
    sigdelset(&set, SIGINT);
    if (sigismember(&set, SIGINT))
    {
        printf("SIGINT是在set集合中\n");
    }
    else
    {
        printf("SIGINT不在set集合中\n");
    }
    return 0;
}

信号阻塞集

        信号阻塞集也称信号屏蔽集、信号掩码。
        每个进程都有一个阻塞集,创建子进程时子进程将继承父进程的阻塞集。
        信号阻塞集用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它, 直到 进程准备好时再将信号通知进程)。 所谓阻塞并不是禁止传送信号, 而是暂缓信号的 传送。若将被阻塞的信号从信号阻塞集中删除, 且对应的信号在被阻塞时发生了,进程 将会收到相应的信号。
        我们可以通过 sigprocmask() 修改当前的信号阻塞集来改变信号的阻塞情况。

sigprocmask函数

        检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信号 阻塞集由 set 指定,而原先的信号阻塞集合由 oldset 保存。
所需头文件
        #include
函数
        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:保存原先信号阻塞集地址
#include 
#include 
#include 
#include 
#include 
int main(int argc, char const *argv[])
{
    // 定义一个信号集
    // vscode中编写的sigset_t会报错,不管
    sigset_t set;
    // 清空set集
    sigemptyset(&set);
    // 将SIGINT添加到set集合中
    sigaddset(&set, SIGINT);
    // 将set集合添加到阻塞集
    // vscode中编写的SIG_BLOCK,SIG_BLOCK会报错不管
    sigprocmask(SIG_BLOCK, &set, NULL);
    printf("SIGINT信号将在5秒后从阻塞集中删除\n");
    sleep(5);
    sigprocmask(SIG_UNBLOCK, &set, NULL);
    while (1)
        ;
    return 0;
}

你可能感兴趣的:(Linux高级系统编程,linux,C,c语言)