linux内核学习1:新进程间通信

2021/7/28 7:06:08

本文主要是介绍linux内核学习1:新进程间通信,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1. 管道 (PIPE)

管道实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。

管道的特点:

  1. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
  2. 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。 比如fork或exec创建的新进程,在使用exec创建新进程时,需要将管道的文件描述符作为参数传递给exec创建的新进程。当父进程与使用fork创建的子进程直接通信时,发送数据的进程关闭读端,接受数据的进程关闭写端。
  3. 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
  4. 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。

管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。 管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

管道只能在本地计算机中使用,而不可用于网络间的通信。

使用格式为:

#include<unistd.h>
int  pipe(int fd[2]);

功能: 创建一个简单的管道,若成功则为数组fd分配两个文件描述符,其中fd[0] 用于读取管道,fd[1]用于写入管道。
返回:成功返回0,失败返回-1;
(1)管道通信是单向的,并且遵守先进先出的原则,即先写入的数据先读出。
(2)管道是一个无结构,无固定大小的字节流。
(3) 管道把一个进程的标准输出和另一个进程的标准输入连接在一起。数据读出后就意味着从管道中移走了,消失了。其它的进程都不能
再读到这些数据。就像我们平常见到的管子水流走了就没有了。 这点很重要!!
(4) pipe这种管道用于两个有亲缘关系的进程之间。eg:父子进程…
在这里插入图片描述

  1. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。

  2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。

  3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。

#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
void sys_err(const char *str)
{
    perror(str);
    exit(1);
}
int main(void)
{
    pid_t pid;
    char buf[1024];
    int fd[2];
    char *p = "test for pipe\n";
    
   if (pipe(fd) == -1)
       sys_err("pipe");
   pid = fork();
   if (pid < 0) {
       sys_err("fork err");
   } else if (pid == 0) {
        close(fd[1]);
        int len = read(fd[0], buf, sizeof(buf));
        write(STDOUT_FILENO, buf, len);
        close(fd[0]);
   } else {
       close(fd[0]);
       write(fd[1], p, strlen(p));
       wait(NULL);
       close(fd[1]);
   }
    
    return 0;
}

2. 命名管道(FIFO)

也叫有名管道、FIFO 文件。允许没有亲缘关系的进程间通信。
以 FIFO 的文件形式存在于文件系统中,这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。

命名管道(FIFO)和无名管道(pipe)有一些特点是相同的,不一样的地方在于:

1、FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在内存中。
2、当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
3、FIFO 有名字,不相关的进程可以通过打开命名管道进行通信。

命名管道的创建
所需头文件:

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

int mkfifo( const char *pathname, mode_t mode);
功能:
命名管道的创建。
参数:

pathname: 普通的路径名,也就是创建后 FIFO 的名字。 mode: 文件的权限,与打开普通文件的 open() 函数中的
mode 参数相同,相关说明请点此链接。 返回值:

成功:0
失败:如果文件已经存在,则会出错且返回 -1。

1. #include <stdio.h>  
2. #include <sys/types.h>  
3. #include <sys/stat.h>  
4.   
5. int main(int argc, char *argv[])  
6. {  
7.     int ret;  
8.       
9.     ret = mkfifo("my_fifo", 0666); // 创建命名管道  
10.     if(ret != 0){   // 出错  
11.         perror("mkfifo");  
12.     }  
13.       
14.     return 0;  
15. }  

在这里插入图片描述
后期的操作,把这个命名管道当做普通文件一样进行操作:open()、write()、read()、close()。但是,和无名管道一样,操作命名管道肯定要考虑默认情况下其阻塞特性。

细看:
https://blog.csdn.net/lianghe_work/article/details/47722175

下面验证的是默认情况下的特点,即 open() 的时候没有指定非阻塞标志( O_NONBLOCK )。

1) open() 以只读方式打开 FIFO 时,要阻塞到某个进程为写而打开此 FIFO open() 以只写方式打开 FIFO
时,要阻塞到某个进程为读而打开此 FIFO。 简单一句话,只读等着只写,只写等着只读,只有两个都执行到,才会往下执行。
2)假如 FIFO 里没有数据,调用 read() 函数从 FIFO 里读数据时 read() 也会阻塞。这个特点和无名管道是一样的。
3)通信过程中若写进程先退出了,就算命名管道里没有数据,调用 read() 函数从 FIFO 里读数据时不阻塞;若写进程又重新运行,则调用
read() 函数从 FIFO 里读数据时又恢复阻塞。
4)通信过程中,读进程退出后,写进程向命名管道内写数据时,写进程也会(收到SIGPIPE 信号)退出。 5)调用 write() 函数向 FIFO 里写数据,当缓冲区已满时 write() 也会阻塞。
4和5这两个特点和无名管道是一样的,这里不再验证,详情请看《无名管道》。

命名管道非阻塞标志操作

命名管道可以以非阻塞标志(O_NONBLOCK)方式打开:
非阻塞标志(O_NONBLOCK)打开的命名管道有以下特点:

  1. 先以只读方式打开,如果没有进程已经为写而打开一个 FIFO, 只读 open() 成功,并且 open() 不阻塞。
  2. 先以只写方式打开,如果没有进程已经为读而打开一个 FIFO,只写 open() 将出错返回 -1。
  3. read()、write() 读写命名管道中读数据时不阻塞。

3. 信号 (signal)

signal机制可以被理解成进程的软中断, 用信号处理来模拟操作系统的中断功能

软中断是执行中断指令产生的,而硬中断是由外设引发的。https://zhuanlan.zhihu.com/p/85597791
信号全称为软中断信号,也有人称软中断。

软中断信号(signal,又简称为信号)用来通知进程发生了异常事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。

在这里插入图片描述
signal的执行点可以理解成从内核态返回用户态时,在返回时,如果发现待执行进程存在被触发的signal,那么在离开内核态之后(也就是将CPU切换到用户模式),执行用户进程为该signal绑定的signal处理函数,从这一点上看,signal处理函数是在用户进程上下文中执行的。当执行完signal处理函数之后,再返回到用户进程被中断或者system call(软中断或者指令陷阱)打断的地方。

Signal机制实现的比较灵活,用户进程由于中断或者system call陷入内核之后,将断点信息都保存到了堆栈中,在内核返回用户态时,如果存在被触发的signal,那么直接将待执行的signal处理函数push到堆栈中,在CPU切换到用户模式之后,直接pop堆栈就可以执行signal处理函数并且返回到用户进程了。Signal处理函数应用了进程上下文,并且应用实际的中断模拟了进程的软中断过程。

如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检查是否收到信号的实际是:一个进程在即将从内核态返回到用户态时,或者在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。

内核处理一个进程收到的信号实际是在一个进程从内核态返回用户态时,所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理

内 核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。

参考:https://blog.csdn.net/Thanksgining/article/details/41824475?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control

3.1 信号列表

列表中,编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

  1. SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
  2. SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
  3. SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
  4. SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
  5. SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
  6. SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
  7. SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
  8. SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1
  9. SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5
  10. SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9
  11. SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
  12. SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13
  13. SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9
  14. SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5
  15. SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1
  16. SIGRTMAX

其中,举例几个常见的信号:
SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
SIGABRT
调用abort函数生成的信号。
SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
kill -9 pid 、kill -SIGKILL
SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
SIGTERM
程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。
kill pid、kill -15 pid 、kill -SIGTERM
SIGCHLD
子进程结束时, 父进程会收到这个信号。
如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。
SIGSYS
非法的系统调用。

在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
不能恢复至默认动作的信号有:SIGILL,SIGTRAP
默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。

3.2 信号的发送

3.2.1 信号来自内核, 生成信号的请求来自以下3个地方

(1)用户
用户可以通过输入Ctrl-C, Ctrl-\等命令,或是终端驱动程序分配给信号控制字符的其他任何键来请求内核产生信号。
(2)内核 当进程执行出错时, 内核给进程发送一个信号。
例如,非法段存取,浮点数溢出,亦或是一个非法指令,内核也利用信号通知进程特定事件发生。
(3)进程
一个进程可以通过系统调用kill给另外一个进程发送信号, 一个进程可以和另一个进程通过信号通信。

发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。
下面只举kill的用法

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,int signo)

该系统调用可以用来向任何进程或进程组发送任何信号。参数pid的值为信号的接收进程

pid>0 进程ID为pid的进程
pid=0 同一个进程组的进程
pid<0 pid!=-1 进程组ID为 -pid的所有进程
pid=-1 除发送进程自身外,所有进程ID大于1的进程

Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。
Kill()最常用于pid>0时的信号发送。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。下面是一些可能返回的错误代码:

EINVAL:指定的信号sig无效。
ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程。
EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID 或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。如果参数pid小于-1,即该信号发送给一个组,则该错误表示组中有成员进程不能接收该信号。

3.3 信号捕获处理

进程能够通过系统调用signal告诉内核, 它要如何处理信号, 进程有3个选择。

#define SIG_DFL ((void (*) (int)) 0) *语句A*
#define SIG_IGN ((void (*) (int)) 1)
#define SIG_ERR ((void (*) (int)) -1)
SIG_ERR(-1):信号处理函数里, 失败返回SIG_ERR。
SIG_DFL(0):默认信号处理程序
SIG_IGN(1):忽略信号的处理程序

(1)接收默认处理(通常是消亡)
例如,SIGINT的默认处理是消亡, 进程并不一定要使用signal接收默认处理,但是进程能够通过以下调用来恢复默认处理。
signal(SIGINT, SIG_DFL);
(2)忽略信号
程序可以通过以下调用来告诉内核, 它需要忽略SIGINT。
signal(SIGINT, SIG_IGN);
(3)信号处理函数
程序能够告诉内核,当程序到来时应该调用哪个函数。
signal(signum, functionname);

在这里插入图片描述
SIG_ERR:信号处理函数里, 失败返回SIG_ERR。

3.3.1 信号处理例子

(1)默认处理信号

#include<stdio.h>
#include<signal.h>
          
int main()
{             
        signal(SIGINT,SIG_DFL);
        int i;
        for( i = 0; i<10;++i)
        {
                printf("hello world\n");
                sleep(1);
        }
        return 0;
}

(2)忽略信号

#include<stdio.h>
#include<signal.h>
          
int main()
{          
        signal(SIGINT,SIG_IGN);
        int i;
        for( i = 0; i<10;++i)
        {
                printf("hello world\n");
                sleep(1);
        }
        return 0;
}

3)使用信号处理处理函数

#include<stdio.h>
#include<signal.h>
          
int main()
{         
        void f(int);      
        signal(SIGINT,f);
        int i;
        for( i = 0; i<10;++i)
        {
                printf("hello world\n");
                sleep(1);
        }
        return 0;
}   
    
void f(int signum)
{   
        printf("SIGINT\n");
}

3.4 sigaction函数使用

信号中,signal是初级用法,sigaction是高级用法

我们已经成功完成了信号的收发,那么为什么会有高级版出现呢?其实之前的信号存在一个问题就是,虽然发送和接收到了信号,可是总感觉少些什么,既然都已经把信号发送过去了,为何不能再携带一些数据呢?
正是如此,我们需要另外的函数来通过信号传递的过程中,携带一些数据。咱么先来看看发送的函数吧。

#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction {
   void       (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
   void       (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用
   sigset_t   sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
   int        sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
};//回调函数句柄sa_handler、sa_sigaction只能任选其一

signum参数指出要捕获的信号类型,act参数指定新的信号处理方式,oldact参数输出先前信号的处理方式(如果不为NULL的话)。

sigaction 是一个系统调用,根据这个函数原型,我们不难看出,在函数原型中,第一个参数signum应该就是注册的信号的编号;第二个参数act如果不为空说明需要对该信号有新的配置;第三个参数oldact如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。

信号处理

struct sigaction act;
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;    //如果设置了SA_SIGINFO属性,说明使用的处理函数是sa_sigaction,而不是sa_handler,否则,系统会默认使用 sa_handler 所指向的信号处理函数。
sigaction(SIGIO, &act, NULL);

//sa_sigaction 和 sa_handler 使用的是同一块内存空间,相当于 union,所以只能设置其中的一个,不能两个都同时设置。

举例:

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


//void (*sa_sigaction)(int, siginfo_t *, void *);void handler(int signum, siginfo_t * info, void * context){
    if(signum == SIGIO)
        printf("SIGIO   signal: %d\n", signum);
    else if(signum == SIGUSR1)
        printf("SIGUSR1   signal: %d\n", signum);
    else
        printf("error\n");
    
    if(context)
    {
        printf("content: %d\n", info->si_int);
        printf("content: %d\n", info->si_value.sival_int);
    }}


int main(void){
    //int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    struct sigaction act;
    
    /*
     struct sigaction {
     void     (*sa_handler)(int);
     void     (*sa_sigaction)(int, siginfo_t *, void *);
     sigset_t   sa_mask;
     int        sa_flags;
     };
     */
    act.sa_sigaction = handler;
    act.sa_flags = SA_SIGINFO;
    
    sigaction(SIGIO, &act, NULL);
    sigaction(SIGUSR1, &act, NULL);
    for(;;)
    {
        sleep(10000);
    }
    return 0;}

信号发送函数

#include <signal.h>int sigqueue(pid_t pid, int sig, const union sigval value);union sigval {
   int   sival_int;
   void *sival_ptr;
};

使用这个函数之前,必须要有几个操作需要完成

  • 如果要获取数据,使用 sigaction 函数安装信号处理程序时,制定SA_SIGINFO 的标志。
  • sigaction 结构体中的 sa_sigaction 成员提供了信号捕捉函数。如果实现的是 sa_handler 成员,那么将无法获取额外携带的数据。

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。可以使用 value 参数向信号处理程序传递整数值或者指针值。

sigqueue 函数不但可以发送额外的数据,还可以让信号进行排队(操作系统必须实现了 POSIX.1的实时扩展),对于设置了阻塞的信号,使用 sigqueue 发送多个同一信号,在解除阻塞时,接受者会接收到发送的信号队列中的信号,而不是直接收到一次。

但是,信号不能无限的排队,信号排队的最大值受到SIGQUEUE_MAX的限制,达到最大限制后,sigqueue 会失败,errno 会被设置为 EAGAIN。

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


int main(int argc, char** argv){
    if(4 != argc)
    {
        printf("[Arguments ERROR!]\n");
        printf("\tUsage:\n");
        printf("\t\t%s <Target_PID> <Signal_Number> <content>\n", argv[0]);
        return -1;
    }
    int pid = atoi(argv[1]);
    int sig = atoi(argv[2]);

    if(pid > 0 && sig > 0)
    {
        //int sigqueue(pid_t pid, int sig, const union sigval value);
        union sigval val;
        val.sival_int = atoi(argv[3]);
        printf("send: %d\n", atoi(argv[3]));
        sigqueue(pid, sig, val);
    }
    else
    {
        printf("Target_PID or Signal_Number MUST bigger than 0!\n");
    }
    
    return 0;}

在这里插入图片描述

4. 消息队列(Message queues)



这篇关于linux内核学习1:新进程间通信的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程