title: 函数对象
date: 2020-11-04 22:12:23
tags: linux
category: linux

进程通信

1.进程通信的概述

1.1进程通信的目的:

1.数据传输:一个进程要将它的数据发送给另一个进程
2.资源共享:多个进程之间共享资源
3.通知事件:一个进程需要通知另一个进程某一个事件已经发生
4.进程控制:有些进程希望控制另一个进程的执行(如Debug进程),此时控制进程希望拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

5.共享数据:多个进程之间共享数据时,一个进程对共享数据进行了修改,另一个进程应该立马能看见

2.管道通信

2.1管道性质:

​ 管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。其本质是一个伪文件(实为内核缓冲区),由两个文件描述符引用,一个表示读端,一个表示写端。规定数据从管道的写端流入管道,从读端流出

2.2管道的原理:

管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。

2.3管道的局限性:

① 数据自己读不能自己写。

② 数据一旦被读走,便不在管道中存在,不可反复读取。

③ 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。

④ 只能在有公共祖先的进程间使用管道。

2.4pipe函数

int pipe(int pipefd[2]); 成功:0;失败:-1,设置errno

函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。

img

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

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

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

    2.5管道行为:

    使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):

    1. 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。

    2. 如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。

    3. 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。

    4. 如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。

      2.6管道读写

      ① 读管道: 1. 管道中有数据,read返回实际读到的字节数。

    5. 管道中无数据:

      (1) 管道写端被全部关闭,read返回0 (好像读到文件结尾)

      (2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)

      ② 写管道: 1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)

    6. 管道读端没有全部关闭:

      (1) 管道已满,write阻塞。

      (2) 管道未满,write将数据写入,并返回实际写入的字节数。

      2.7无名管道

      无名管道:管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。

      单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。

      数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      #include "stdio.h"
      #include "stdlib.h"
      #include "string.h"
      #include "sys/types.h"
      #include "sys/wait.h"
      #include "unistd.h"

      int main() {
      pid_t child;
      int fd[2];
      int res = pipe(fd);
      child = fork();
      if (res == -1) {
      perror("the pipe create error");
      exit(EXIT_FAILURE);
      }
      if (child < 0) {
      perror("fork error");
      exit(EXIT_FAILURE);
      }
      if (child == 0) {
      close(fd[0]);
      write(fd[1], "the child message\n", 20);
      close(fd[1]);
      } else {
      close(fd[1]);
      char buf[20];
      read(fd[0], buf, 20);
      printf("%s", buf);
      wait(NULL);
      close(fd[0]);
      }
      return 0;
      }

      2.8有名管道

      有名管道存在于文件系统中,提供写入原子性特征,共享内存效率高

      有名管道的特征:
      a、有名字,储存于普通文件系统中
      b、任何具有相应权限的进程都可以使用open()来获取FIFO的文件描述符
      c、跟普通文件一样,用read()和writ()来读和写
      d、不能用lseek来定位
      e、具有写入原子性,支持多写者同时进行写操作而数据不会相互践踏

      read.c

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      #include <errno.h>
      #include <fcntl.h>
      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <sys/stat.h>
      #include <sys/types.h>
      #include <unistd.h>

      int main() {
      int fd;
      fd = open("fifo", O_RDONLY);
      char buf[40];
      int i = 0;
      while (1) {
      if (i < 10)
      i++;
      else {
      break;
      }
      read(fd, buf, 40);
      printf("%s\n", buf);
      sleep(3);
      }
      exit(EXIT_SUCCESS);
      }

      write.c

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      #include <errno.h>
      #include <fcntl.h>
      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <sys/stat.h>
      #include <sys/types.h>
      #include <time.h>
      #include <unistd.h>
      int main() {
      time_t t;
      int fres = mkfifo("fifo", 0777);
      int fd;
      if (fres != 0) {
      perror("create fifo failure\n");
      exit(EXIT_FAILURE);
      }

      fd = open("fifo", O_WRONLY);
      int i = 0;
      char buf[40];
      while (1) {
      if (i < 10) {
      i++;
      } else {
      break;
      }
      time(&t);
      sprintf(buf, "wrfifo %d sends %s", getpid(), ctime(&t));
      write(fd, buf, 40);
      sleep(3);
      }
      exit(EXIT_SUCCESS);
      }

3.信号signal

3.1信号的本质

软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。

3.2信号的处理

收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:

第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。

第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。

第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。

3.3实时信号和非实时信号

早期Unix系统只定义了32种信号,前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。

非实时信号都不支持排队,都是不可靠信号实时信号都支持排队,都是可靠信号

3.4信号的处理流程

3.41信号的诞生

信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

这里按发出信号的原因简单分类,以了解各种信号:

(1) 与进程终止相关的信号。当进程退出,或者子进程终止时,发出这类信号。

(2) 与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其他各种硬件错误。

(3) 与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽。

(4) 与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。

(5) 在用户态下的进程发出的信号。如进程调用系统调用kill向其他进程发送信号。

(6) 与终端交互相关的信号。如用户关闭一个终端,或按下break键等情况。

(7) 跟踪进程执行的信号。

3.42信号在目标进程中注册

在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号。内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。如果信号发送给一个正在睡眠的进程,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。如果发送给一个处于可运行状态的进程,则只置相应的域即可。

进程的task_struct结构中有关于本进程中未决信号的数据成员: struct sigpending pending:

1
2
3
4
5
6
7
struct sigpending{

struct sigqueue *head, *tail;

sigset_t signal;

};

第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为”未决信号信息链”)的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:

1
2
3
4
5
6
7
struct sigqueue{

struct sigqueue *next;

siginfo_t info;

}

信号在进程中注册指的就是信号值加入到进程的未决信号集sigset_t signal(每个信号占用一位)中,并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。

当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做”可靠信号”。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册)。

当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册(通过sigset_t signal指示),则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做”不可靠信号”。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构。

总之信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)

3.43信号的执行和注销

内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。当其由于被信号唤醒或者正常调度重新获得CPU时,在其从内核空间返回到用户空间时会检测是否有信号等待处理。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。

对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则执行完相应的处理函数后应该把信号在进程的未决信号集中删除(信号注销完毕)。否则待该信号的所有sigqueue处理完毕后再在进程的未决信号集中删除该信号。

当所有未被屏蔽的信号都处理完毕后,即可返回用户空间。对于被屏蔽的信号,当取消屏蔽后,在返回到用户空间时会再次执行上述检查处理的一套流程。

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

处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似的继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。

3.44信号的安装

如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。

linux主要有两个函数实现信号的安装:signal()、sigaction()。其中signal()只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。

3.5几个常用的信号函数

3.5.1kill

功能描述用于向任何进程组或进程发送信号。

头文件

1
2
3
4
5
#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid, int sig);

参数

1
2
3
4
5
6
pid:可能选择有以下四种

1. pid大于零时,pid是信号欲送往的进程的标识。
2. pid等于零时,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。
3. pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)。
4. pid小于-1时,信号将送往以-pid为组标识的进程。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "fcntl.h"
#include "signal.h"
#include "stdio.h"
#include "stdlib.h"
#include "sys/stat.h"
#include "sys/types.h"
#include "sys/wait.h"
#include "unistd.h"

static int p;

void hander() { kill(p, SIGINT); }//像pid为p的进程发送SIGINT信号(CTRL C)

void child() {
while (1) {
printf("Im the child , my pid = %d\n", getpid());
sleep(2);
}
}

int main() {
pid_t pid = fork();
p = pid;
if (pid > 0) {
while (1) {
signal(SIGIO, hander);
printf("Im father process,waiting child my now %d \n", getpid());
// wait(0);
sleep(4);
}
} else {
child();
}
printf("the child coming!i willing be going...\n");
sleep(2);
printf("by!\n");
sleep(1);
return 0;
}

3.5.2raise

头文件及函数定义

1
2
3
#include <signal.h>

int raise(int signo)

函数作用

向进程本身发送信号,参数为即将发送的信号值。调用成功返回 0;否则,返回 -1。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include "fcntl.h"
#include "signal.h"
#include "stdio.h"
#include "stdlib.h"
#include "sys/stat.h"
#include "sys/types.h"
#include "sys/wait.h"
#include "unistd.h"

void child() {
while (1) {
sleep(1);
printf("Im child ,my pid = %d\n", getpid());
sleep(1);
int i = 5;
for (i = 5; i > 0; i--) {
printf("%d s ago,i will go die\n", i);
sleep(1);
}
raise(SIGINT);
}
}

int main() {
pid_t pid = 0;
pid = fork();
if (pid == 0) {
child();
} else {
printf("Im Father,My pid = %d\n", getpid());
wait(NULL);
}
printf("bye\n");
return 0;
}

3.5.3alarm

使用 alarm 函数可以设置一个时间值 ( 闹钟时间 ),当所设置的时间到了时,产生 SIGALRM 信号

如果不能扑捉此信号,则默认动作是终止该进程.

函数: unsigned int alarm ( unsigned int seconds )

经过了指定的 seconds 秒后会产生信号 SIGALRM.

每个进程只能有一个闹钟时间,如果在调用 alarm 时,以前已为该进程设置过闹钟时间,而且它还没有

超时,以前等级的闹钟时间则被新值替换.

如果有以前登记的尚未超时的闹钟时间,而这次 seconds 值是0 ,则表示取消以前的闹钟.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void header() {
printf("get signal !will be exit!\n");
exit(EXIT_SUCCESS);
}

int main() {
signal(SIGALRM, header);
alarm(3);
int i = 3;
for (; i > 0; i--) {
printf("%d s send alarm\n", i);
sleep(1);
}
return 0;
}

3.6sigaction信号集

头文件和函数原型

1
2
3
#include <signal.h>

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

作用

sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。

第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;

第三个参数oldact指向的对象用来保存返回的原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数设为NULL,那么该函数可用于检查信号的有效性

第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些信号等等。

sigaction结构体定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct sigaction {

union{

__sighandler_t _sa_handler;

void (*_sa_sigaction)(int,struct siginfo *, void *);

}_u

sigset_t sa_mask;

unsigned long sa_flags;

}

1、联合数据结构中的两个元素_sa_handler以及_sa_sigaction指定信号关联函数,即用户指定的信号处理函数。除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。

2、由_sa_sigaction是指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。

3、sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位。

注:请注意sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞。

4、sa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另一个比较重要的标志位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误。

3.7信号集及信号集操作函数:

信号集被定义为一种数据类型:

1
2
3
4
5
typedef struct {

unsigned long sig[_NSIG_WORDS];

} sigset_t

信号集用来描述信号的集合,每个信号占用一位。Linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。下面是为信号集操作定义的相关函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum)

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

sigemptyset(sigset_t *set)初始化由set指定的信号集,信号集里面的所有信号被清空;

sigfillset(sigset_t *set)调用该函数后,set指向的信号集中将包含linux支持的64种信号;

sigaddset(sigset_t *set, int signum)在set指向的信号集中加入signum信号;

sigdelset(sigset_t *set, int signum)在set指向的信号集中删除signum信号;

sigismember(const sigset_t *set, int signum)判定信号signum是否在set指向的信号集中。

3.8 信号阻塞与信号未决:

每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。下面是与信号阻塞相关的几个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset));

int sigpending(sigset_t *set));

int sigsuspend(const sigset_t *mask));

sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种:

SIG_BLOCK 在进程当前阻塞信号集中添加set指向信号集中的信号

SIG_UNBLOCK 如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞

SIG_SETMASK 更新进程阻塞信号集为set指向的信号集

sigpending(sigset_t *set))获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。

sigsuspend(const sigset_t *mask))用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。

3.9示例代码(信号集)

4.共享内存

5.消息队列

6.套接字socket