通信

进程间通信就是在不同进程之间传播或交换信息。

进程间交换的信息可多可少,少者仅状态或数值,多者可交换成千上万字节。

  • 分类
    • 低级通信[^9]
      • 信号量机制
      • 信号机制
    • 高级通信[^10]
      • 管道机制
      • 消息队列机制
      • 共享内存机制
      • Socket,RPC

下面开始分别描述几种通信:

  • 低级通信 - 信号机制

信号相当于向进程发送一个通知,通知某事件发生

收到信号的进程会立即执行指定的操作

信号发出者

  1. 进程
  2. 系统(含硬件)

信号来源

  1. 键盘输入特殊组合键产生信号,例:“Ctrl + C"
  2. 执行终端命令产生信号,例: kill系列命令
  3. 程序中调用函数产生信号,例: kill()、abort( )
  4. 硬件异常或内核产生相应信号。例:内存访问错误

*Linux信号设计

Linux信号有64个,每个信号都对应一个正整数常量,称为signal number,即信号编号。定义在系统头文件<signal.h>中 。可使用kill -l查看所有信号量。

每个进程在运行时,都要通过信号机制来检查是否有信号到达。若有,便中断正在执行的程序,转向与该信号相对应的处理程序,以完成对该事件的处理;处理结束后再返回到原来的断点继续执行。

yinfeng@DESKTOP-AFSSCD2:~$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX
名 字 说 明
01 SIGHUP 挂起(hangup)
02 SIGINT 中断,当用户从键盘按c键或break键时
03 SIGQUIT 退出,当用户从键盘按quit键时
04 SIGILL 非法指令
05 SIGTRAP 跟踪陷阱(trace trap),启动进程,跟踪代码的执行
06 SIGIOT IOT指令
07 SIGEMT EMT指令
08 SIGFPE 浮点运算溢出
09 SIGKILL 杀死、终止进程
10 SIGBUS 总线错误
11 SIGSEGV 段违例(segmentation violation),进程试图去访问其虚地址空间以外的位置
12 SIGSYS 系统调用中参数错,如系统调用号非法
13 SIGPIPE 向某个非读管道中写入数据
14 SIGALRM 闹钟。当某进程希望在某时间后接收信号时发此信号
15 SIGTERM 软件终止(software termination)
16 SIGUSR1 用户自定义信号1
17 SIGUSR2 用户自定义信号2
18 SIGCLD 某个子进程死
19 SIGPWR 电源故障

*Linux信号机制编程

signal( );	// 注册信号处理的函数
/*
---系统调用格式---     
signal(Sig,function)
Sig:用于指定信号的类型。
function:自定义信号处理函数。

---功能---
预置对信号的处理方式,允许调用进程控制软中断信号
为指定信号注册信号处理函数。当进程收到Sig信号时,立即自动调用function函数执行。
一般在进程初始化时使用该函数注册信号处理函数。
*/
 

kill( );	// 发送信号函数
/*
---系统调用格式---  
int kill(pid,sig)
pid:接收信号的目标进程ID
sig:待发送的信号

---举例---
kill(p1,16);	/*向p1进程发送信号16*/
*/
//////////////////////////////////////////////////////////////////
/////////////////////////////信号示例//////////////////////////////
/*
任务:编写一个死循环的程序,当其收到键盘按下的“CTRL+C”信号后,输出自己的提示信息,如“BYE BYE!”,然后退出。

思路:
	①让进程对“CTRL+ C”的SIGINT信号用自定义的信号处理函数响应。 
	②自定义信号处理函数功能:输出“BYE BYE!"后结束。 
	③在程序中先用signal函数建立SIGINT信号与自定义信号处理函数之间的对应关系。
*/

void byebye()
{
    printf("\nByeBye!hHave a nice day!\n");
    exit(-1);
}

int mian()
{
    signal(SIGINT,byebye);
    while(1)
    {
        printf("This is operating system class\n");
        sleep(2);
    }
    return 0;
}

运行结果:

image-20230311114212494

举例

①:使用Ctrl + C杀死一个进程

image-20230311105903731

背后的原理

  1. 按下Ctrl+C产生信号SIGINT
  2. 进程收到SIGINT,执行默认操作(结束进程)

②:使用Ctrl + Z挂起(暂停)一个进程

背后的原理

  1. "Ctrl + Z"产生信号SIGTSTP
  2. 进程收到SIGTSTP信号(20号),执行默认操作(挂起进程)

拓展:前台继续运行:fg;后台继续运行:bg


  • 高级通信 - 管道机制(pipe)

管道,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。

向管道(共享文件)提供输入的发送进程(即写进程)以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程)则从管道中接收(读)数据。

这种方式首创于UNIX系统

管道机制需要具备的协调能力

  1. 互斥:不可以同时读写pipe
  2. 同步:写者写完就睡眠,读者读完就唤醒写者并睡眠
  3. 确定对方是否存在:确定后才可以通信

管道分类

  1. 有名管道

    一个可以在文件系统中长期存在的、具有路径名的文件。用系统调用mknod( )建立。它克服无名管道使用上的局限性,可让更多的进程也能利用管道进行通信。因而其它进程可以知道它的存在,并能利用路径名来访问该文件。对有名管道的访问方式与访问其他文件一样,需先用open( )打开。

  2. 无名管道

    一个临时文件。利用pipe( )建立起来的无名文件(无路径名)。只用该系统调用所返回的文件描述符来标识该文件,故只有调用pipe( )的进程及其子孙进程才能识别此文件描述符,才能利用该文件(管道)进行通信。当这些进程不再使用此管道时,核心收回其索引结点。

管道局限性

  1. 只支持单向数据流
  2. 只能用于具有亲缘关系的进程之间
  3. 无名管道没有名字
  4. 管道的缓冲区是有限的(管道只存于内存中,在管道创建时,为缓冲区分配一个页面的大小)
  5. 管道所传送的是无格式字节流。这就要求管道的读出方和写入方必须事先约定好数据的格式。比如多少字节算作一个消息等

*Linux管道机制编程

// 建立管道
pipe( );
/*
---功能---
建立一无名管道

---系统调用格式---
pipe(filedes);

pipe():int pipe(filedes);
filedes:int filedes[2];
其中,filedes[1]是写入端,filedes[0]是读出端
*/
// 读写管道
read(); / write();
/*
---功能---
管道读写

---系统调用格式---
read(fd,buf,nbyte);
write(fd,buf,nbyte);

read():int read(fd,buf,nbyte);
write():ssize_t write(fd,buf,nbytes);

fd:int fd;
buf:char *buf;
nbyte:unsigned nbyte;

read:
从fd所指示的文件中读出nbyte个字节的数据,并将它们送至由指针buf所指示的缓冲区中。
如该文件被加锁,等待,直到锁打开为止
write:
把nbyte 个字节的数据,从buf所指向的缓冲区写到由fd所指向的文件中。
如文件加锁,暂停写入,直至开锁
*/

  • 高级通信 - 消息传递机制

消息传递机制允许进程彼此进行通信,而不必借助于共享数据。可用于进程同步,也可用于交换信息。提供两个原语(系统调用) send和receive。

send(destination, message) // 向目标进程destination发送一条消息message

receive(source, message) // 接收来自进程source的一条消息message

消息传送系统设计涉及同步、寻址、格式和排队规则等多项问题,见下文:

同步

发送者和接收者可以是阻塞方式或非阻塞方式,共有四种组合:

  1. 阻塞发送:指发送方只有在消息已经发送出去之后才能返回。
  2. 阻塞接收:指接收方一直阻塞到消息已被确切地接收到为止。
  3. 非阻塞发送:指发送原语在通知系统将要发送消息缓冲区内的消息后即返回,不必等消息已发送。
  4. 非阻塞接收:指接收原语在通知系统将要接收消息并将其存储于消息缓冲区内后即返回,不必等消息已接收。

寻址

  1. 直接寻址:通信的每一方都必须显式地指明消息接收方和发送方是谁。

  2. 间接寻址消息发送到一个共享的数据结构中,该结构由临时存放消息的队列组成,称为信箱或端口。

    • 信箱

      ① 公用信箱

      ② 共享信箱

      ③ 私有信箱

消息格式

消息格式取决于消息机制的目标和在什么系统上运行。下图格式适用于支持变长消息的操作系统。

image-20230311152934046

排队规则 & *Linux消息队列机制设计:

消息(message)是一个格式化的可变长的信息单元。消息机制允许由一个进程给其它任意的进程发送一个消息。当一个进程收到多个消息时,可将它们排成一个消息队列

消息队列就是一个消息的链表,每个消息队列都有一个队列头,Linux用结构struct msg_queue来描述。队列头中包含了该队列的大量信息,包括消息队列的键值、用户ID、组ID、消息数目、读写进程ID等。其定义如下:

struct msg_queue
{
    struct ipc_perm q_perm;
    time_t q_stime;     // last msgsnd time
    time_t q_rtime;     // last msgrcv time
    time_t q_ctime;     // last change time
    unsigned long q_cbytes;      // current number of bytes on queue
    unsigned long q_qnum;        // number of message in queue
    unsigned long q_qbytes;      // max number of bytes on queue
    pid_t q_lspid;     // pid of last msgsnd
    pid_t q_lrpid;     // last receive pid
    struct list_head q_messages;
    struct list_head q_receives;
    struct list_head q_senders;
};

Linux的消息队列机制涉及四个系统调用:

1、msgget();
/*
---功能---
创建或使用一个消息,获得一个消息的描述符

---系统调用格式---
msgqid = msgget(key,flag);
key:用户指定的消息队列的名字;
flag:用户设置的标志和访问方式。如:
IPC_CREAT |0660       是否该队列已被创建。无则创建,是则打开;
IPC_EXCL |0660        是否该队列的创建应是互斥的。
msgqid:系统调用返回的描述符,非零整数。失败则返回-1
*/

#define IPC_CREAT 01000 	/* Create key if key does not exist. */
#define IPC_EXCL 02000 		/* Fail if key exists. */
#define IPC_NOWAIT 04000 	/* Return error on wait. */
2、msgsnd();
/*
---功能---
发送一消息。向指定的消息队列发送一个消息,并将该消息链接到该消息队列的尾部。


---系统调用格式---
msgsnd(msgqid,msgp,size,flag)
msgqid:消息队列的描述符;
msgp:一个指向准备发送消息的指针,但是消息的数据结构要求是结构体,包括消息类型和消息正文;
size:指示由msgp指向的消息的长度。
flag:规定当核心用尽内部缓冲空间时应执行的动作:进程是等待还是立即返回。
										若是设置IPC_NOWAIT,msgsnd立即返回;
										否则(即为0)调用msgsnd进程睡眠。
*/
3、msgrcv();
/*
---功能---
接受一消息。从指定的消息队列中接收指定类型的消息

---系统调用格式---
msgrcv(msgqid,msgp,size,type,flag)
msgqid:消息队列的描述符;
msgp:一个指向准备发送消息的指针,但是消息的数据结构要求是结构体,包括消息类型和消息正文;
size:指示由msgp指向的消息的长度。
flag:规定当核心用尽内部缓冲空间时应执行的动作:进程是等待还是立即返回
type:规定要读的消息类型,分成三种情况处理:
	type=0,接收该队列的第一个消息,并将它返回给调用者;
	type为正整数,接收类型type的第一个消息;
	type为负整数,接收小于等于type绝对值的最低类型的第一个消息。
*/
4、msgctl();
/*
---功能---
消息队列的操纵。读取消息队列的状态信息并进行修改,
如查询消息队列描述符、修改它的许可权及删除该队列等。

---系统调用格式---
int msgctl(msgqid,cmd,buf);
函数调用成功时返回0,不成功则返回-1。
msgqid:消息队列的描述符;
buf:用户缓冲区地址,供用户存放控制参数和查询结果;
cmd:规定的命令,可分三类:
	(1)IPC_STAT。查询有关消息队列情况的命令。如查询队列中的消息数目、队列中的最大字节数、最后一个发送消息的进程标识符、发送时间等;
	(2)IPC_SET。按buf指向的结构中的值设置和改变有关消息队列属性的命令。如改变消息队列的用户标识符、消息队列的许可权等;
	(3)IPC_RMID。删除消息队列。
*/

举例

///////////////////////////////////////////////////////////////
/////////////////用消息传递方式解决生产者-消费者问题////////////////
/*
思路:阻塞接收?
假设所有的消息都有同样的大小,并且在尚未接收到发出的消息时,由操作系统自动进行缓冲。
在该解决方案中共使用N条消息,消费者首先将N条空消息发送给生产者。
当生产者向消费者传递一个数据项时,它取走一条空消息并送回一条填充了内容的消息。
如果生产者的速度比消费者快,则所有的消息最终都将被填满,等待消费者,生产者将被阻塞,等待返回一条空消息。
如果消费者速度快,则情况正好相反:所有的消息均为空,等待生产者来填充它们,消费者被阻塞,以等待一条填充过的消息。
*/

#define N 100                  	/* 缓冲区个数 */
void producer(void)
{
	int item;
	message m;                	/* 消息缓冲区 */
	while (TRUE){
		item=produce_item();      /* 生成一些数据放入缓冲区 */
		receive(consumer,&m);      /* 等待一条空消息到达 */
        build_message(&m,item);    /* 构造一条可供发送的消息 */
        send(consumer,&m);	       /* 向消费者发送一个数据项 */
    }
}
void consumer(void)
{
     int item,i;
     message m;
     for (i=0;i<N;i++)
		send(producer,&m);   	/* 发送N条空消息 */
     while (TRUE){
        receive(producer,&m);   /* 接收一条包含数据的消息 */
        item=extract_item(&m);  /* 从消息中提取数据项 */
        send(producer,&m);    	/* 发回空消息作为应答 */
        consume_item(item);   	/* 使用数据项进行操作 */
     }
}

  • 高级通信 - 共享存储机制

共享存储区(Share Memory)是通信速度很高的一种通信机制。 该机制可使若干进程共享主存中的某一个区域,且使该区域出现(映射)在多个进程的虚地址空间中。另一方面,一个进程的虚地址空间中又可连接多个共享存储区,每个共享存储区都有自己的名字。

当进程间欲利用共享存储区进行通信时,必须先在主存中建立一共享存储区,然后将它附接到自己的虚地址空间上。此后,进程对该区的访问操作,与对其虚地址空间的其它部分的操作完全相同。进程之间便可通过对共享存储区中数据的读、写来进行直接通信。

image-20230311162939606

图示列出二个进程通过共享一个共享存储区来进行通信。其中,进程A将建立的共享存储区附接到自己的AA’区域,进程B将它附接到自己的BB’区域。

共享存储区机制未提供对该区进行互斥访问及进程同步的措施,用户需要时,必须自己设置。

*Linux共享存储机制设计

1、shmget()
/*
---功能---
创建、获得一个共享存储区

---系统调用格式---
shmid=shmget(key,size,flag)
key:共享存储区的名字;
size:其大小(以字节计);
flag:用户设置的标志,如IPC_CREAT。IPC_CREAT表示若系统中尚无指名的共享存储区,则由核心建立一个共享存储区;
    若系统中已有共享存储区,便忽略IPC_CREAT。

例:shmid=shmget(key,size,(IPC_CREAT|0400))
   创建一个关键字为key,长度为size的共享存储区
*/

附:

操作允许权 八进制数
用户可读 00400
用户可写 00200
小组可读 00040
小组可写 00020
其它可读 00004
其它可写 00002
控制命令
IPC_CREAT 0001000
IPC_EXCL 0002000
2、shmat()
/*
---功能---
共享存储区的附接。从逻辑上将一个共享存储区附接到进程的虚拟地址空间上。

---系统调用格式---
virtaddr=shmat(shmid,addr,flag)
shmid:共享存储区的标识符;
addr:是用户给定的,将共享存储区附接到进程的虚地址空间;
flag:规定共享存储区的读、写权限,以及系统是否应对用户规定的地址做舍入操作。其值为SHM_RDONLY时,表示只能读;其值为0时,表示可读、可写;其值为SHM_RND(取整)时,表示操作系统在必要时舍去这个地址。
viraddr:该系统调用的返回值,是共享存储区所附接到的进程虚地址。
*/
3、shmdt( )
/*
---功能---
把一个共享存储区从指定进程的虚地址空间断开。

---系统调用格式---
shmdt(addr)
addr:要断开连接的虚地址,亦即以前由连接的系统调用shmat( )所返回的虚地址。
调用成功时,返回0值,调用不成功,返回-1。
*/
4、shmctl( )
/*
---功能---
共享存储区的控制,对其状态信息进行读取和修改。
系统调用格式:
               shmctl(shmid,cmd,buf)
shmid:数据结构;
buf:用户缓冲区地址;
cmd:操作命令。命令可分为多种类型:
    (1)用于查询有关共享存储区的情况。如其长度、当前连接的进程数、共享区的创建者标识符等; shmid-> buf
    (2)用于设置或改变共享存储区的属性。如共享存储区的许可权、当前连接的进程计数等; buf-> shmid
    (3)对共享存储区的加锁和解锁命令;
    (4)删除共享存储区标识符等。
*/

  • 高级通信 - 客户机/服务器系统
  1. 套接字(Socket)

    套接字起源于20世纪70年代加州大学伯克利分校版本的UNIX(即BSD Unix),是UNIX 操作系统下的网络通信接口。一开始,套接字被设计用在同一台主机上多个应用程序之间的通信(即进程间的通信),主要是为了解决多对进程同时通信时端口和物理线路的多路复用问题。随着计算机网络技术的发展以及UNIX 操作系统的广泛使用,套接字已逐渐成为最流行的网络通信程序接口之一。

  2. 远程过程调用和远程方法调用

    远程过程(函数)调用RPC(Remote Procedure Call),是一个通信协议,用于通过网络连接的系统。该协议允许运行于一台主机(本地)系统上的进程调用另一台主机(远程)系统上的进程,而对程序员表现为常规的过程调用,无需额外地为此编程。如果涉及的软件采用面向对象编程,那么远程过程调用亦可称做远程方法调用。

    实际上,远程过程调用的主要步骤是:
    (1) 本地过程调用者以一般方式调用远程过程在本地关联的客户存根(****stub),传递相应的参数,然后将控制权转移给客户存根;
    (2) 客户存根执行,完成包括过程名和调用参数等信息的消息建立,将控制权转移给本地客户进程;
    (3) 本地客户进程完成与服务器的消息传递,将消息发送到远程服务器进程;
    (4) 远程服务器进程接收消息后转入执行,并根据其中的远程过程名找到对应的服务器存根,将消息转给该存根

    (5) 该服务器存根接到消息后,由阻塞状态转入执行状态,拆开消息从中取出过程调用的参数,然后以一般方式调用服务器上关联的过程;
    (6) 在服务器端的远程过程运行完毕后,将结果返回给与之关联的服务器存根;
    (7) 该服务器存根获得控制权运行,将结果打包为消息,并将控制权转移给远程服务器进程;
    (8) 远程服务器进程将消息发送回客户端;
    (9) 本地客户进程接收到消息后,根据其中的过程名将消息存入关联的客户存根,再将控制权转移给客户存根;
    (10) 客户存根从消息中取出结果,返回给本地调用者进程,并完成控制权的转移。