进程间通信

进程间的通信

  • 初级介绍
  • 进程间通信(IPC,InterProcess Communication)[^1]
    • IPC现在主要的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
  • 一、管道
  • 二、FIFO
  • 三、消息队列
    • 关于key值的获取
  • 四、共享内存
  • 五、信号(signal)[^2]
    • 信号处理函数的注册
  • 六、信号量

初级介绍

比如手机上的微信跟别人手机上的微信聊天,它通信的方式是基于网络的

1、无名管道

  • 只能单向操作
  • 只能父子进程操作
  • pipe(fd);
  • 父进程关读端close(fd[0]);,写入数据 fd[1],子进程关写端close(fd[1]);,读出数据 fd[0]
  • 读取的数据,被读到后,管道中就没有内容了

2、命名管道

  • 只能单向操作
  • 创建一个FIFO文件管道,独立于进程存在
  • 能单独多个进程对其操作,需要做定向的单选操作
  • 只读打开FIFO文件,没有内容进程就阻塞,等待只写打开FIFO文件进程,写入数据,只读进程才能读到数据
  • 读取的数据,被读到后,管道中就没有内容了
关写端-读取
关读端-写入
关写端-读取
关读端-写入
父进程
数据
子进程

3、消息队列

  • 可以双向操作,都可以往里放,也都可以拿
  • 在内核中创建一个链表的队列
  • 可以根据索引key值,ID号来找消息队列
  • 要有types类型,mtext内容的结构体
  • 把结构体放入消息队列
  • 当在消息队列中去拿匹配的类型的内容没有时,就会阻塞,等待写入后在拿
  • 消息队列跟进程来说是相互独立的
  • 在不清除msgctl();消息队列的情况下,消息队列里面的信息可以一直存在,但会有内核来管理
  • 容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题

4、共享内存

  • 可以多个进共同操作给定的内存空间
  • 创建共享内存/打开
  • 映射——让进程跟这个内存链接
  • 增删改查内存中的数据
  • 释放共享内存——断开共享内存的链接
  • 删掉共享内存

5、信号

  • 通过接受信号来执行动作
  • 能发送数据
  • 只能单向联系
  • 字符串只能父子进程,共享内存间发送

6、信号量

  • 管理临界资源
  • 对多个进程都要访问的资源进行管理,A访问资源,B阻塞等待
  • 信号量集管理
  • P操作——拿锁
  • V操作——放锁

目的介绍:

进程A,进程B之间应该建立一个特殊的通道,进程A能往里写数据,进程B能读里面的数据,进程B能往里面写数据,进程A能读里面的数据,让它们之间实现真正的数据交互

进程A,进程B放在一台PC就是单机通信,如果进程B放在另一台PC那就的多机之间的进间通信,而它们之间就是通过网来通信的

父子进程的基础版本进程间通信:

  • 父进程运行到某个位置,创建一个子进程,子进程有它的数据空间,父进程,子进程的数据空间是相互独立的。

  • 相互之间父进程不能读取到子进程的数据,子进程也不能读到父进程的数据,它们之间缺少一种方式沟通。

残疾的通信方式:

  • 子进程调用exit(0);退出,父进程调用wait(&status);来等待子进程运行结束,在获取子进程的退出码,这也算一种意义上进程间的通信。只不过这种进程间同信在运行中间的意义不大,因为这也只有exit函数把退出码返回发给父进程而已。

  • exec族函数也可以做到用A进程启动一个B进程,A就不运行,也可以实现简陋基础的信息沟通

这两个方法都是基于fork,exec来实现的,这些方式都相对于真正意义上的通信比较残疾,而真正能很好实现进程间通信的技术是IPC。

进程间通信(IPC,InterProcess Communication)1

是指在不同进程之间传播或交换信息

IPC现在主要的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

以前,UNIX系统IPC是各种进程通信方式的统称,但是其中极少能在所有UNIX系统实现中进行移植,随着POSIX和Open Group(以前的X/Open)标准化的推进和影响的扩大,情况虽已得到改善,但差别仍然存在。

IPC类型 SUS FreeBSD 5.2.1 Linux 2.4.22 Mac OS X 10.3 Solaris 9
半双工管道 · (全) · · (全)
FIFO · · · · ·
全双工管道 允许 ·,UDS opt,UDS UDS ·,UDS
命名全双工管道 XSI可选 UDS opt,UDS UDS ·,UDS
消息队列 XSI · · ·
信号量 XSI · · · ·
共享存储 XSI · · · ·
套接字 · · · · ·
STREAMS XSI可选 opt
  • 管道(包括无名管道和命名管道)、消息队列、信号量、共享存储都是基于单机的通信

  • Socket、Streams是基于网络的通信

以Linux中的C语言编程为例。

一、管道

管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。

1、特点:

  1. 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
  2. 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
  3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
  4. 管道中的数据读取后就没有了,管道中不存储数据。

2、原型:

	#include 
	int pipe(int fd[2]);    // 返回值:若成功返回0,失败返回-1

当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。如下图:

进程间通信_第1张图片
要关闭管道只需将这两个文件描述符关闭即可。

read(fd[0], readBuf, 1024)
write(fd[1], readBuf, 1024)

3、例子
单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。如下图所示:
进程间通信_第2张图片
若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。

示例代码:

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

int main()
{
    int fd[2];  // 两个文件描述符
    pid_t pid;
    char buff[20];

    if(pipe(fd) < 0)  // 创建管道
        printf("Create Pipe Error!\n");

    if((pid = fork()) < 0)  // 创建子进程
        printf("Fork Error!\n");
    else if(pid > 0)  // 父进程
    {
        close(fd[0]); // 关闭读端
        write(fd[1], "hello world\n", 12);
    }
    else
    {
        close(fd[1]); // 关闭写端
        read(fd[0], buff, 20);
        printf("%s", buff);
    }

    return 0;
}

无名管道阻塞案例:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

//int pipe(int pipefd[2]);

int main()
{
        int fd[2];
        pid_t pid;
        char buf[128];

        if(pipe(fd) == -1){
                printf("creat pipe failed!\n");
        }

        pid = fork();
        if(pid < 0){
                printf("creat child failed!\n");
        }else if(pid > 0){
        		sleep(3);
                printf("this is father\n");
                close(fd[0]);
                write(fd[1], "hello from father!", strlen("hello from father!"));
                wait(NULL);
        }
        else{
                printf("this is child\n");
                close(fd[1]);
                read(fd[0], buf, 128);
                printf("read from father:%s\n",buf);
                exit(0);
        }
        return 0;
}
  • 当父进程开始是睡3秒,没有给管道中写入数据时,子进程就会阻塞在读取那,等父进程写入管道内容后,子进程才继续读取管道中的内容
  • 一般无名管道一旦确定了流向,后面都不会对其做更改,不会在运行过程中在改变流向

二、FIFO

FIFO,也称为命名管道,它是一种文件类型。

  1. 特点
    1. FIFO可以在无关的进程之间交换数据,与无名管道不同。
    2. FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
  2. 原型
#include 
// 返回值:成功返回0,出错返回-1
int mkfifo(const char *pathname, mode_t mode);

其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。

当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

  • 若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。

  • 若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。

创建FIFO管道:

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

//int mkfifo(const char *pathname, mode_t mode);

int main()
{
        mkfifo("./file",0600);
        return 0;
}

判断要创建的FIFO文件存在的代码优化:

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

//int mkfifo(const char *pathname, mode_t mode);

int main()
{
        int ret = mkfifo("./file",0600);
        if(ret == 0){
                printf("mkfifo suscceess\n");
        }
        if(ret == -1){
                printf("mkfifo failuer\n");
                perror("why");
        }

        return 0;
}

判断优化:


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

//int mkfifo(const char *pathname, mode_t mode);

int main()
{
        if((mkfifo("./file",0600) == -1) && errno == EEXIST ){
                printf("mkfifo failuer\n");
                perror("why");
        }
        else{
                if(errno == EEXIST){
                        printf("This file exists!\n");
                }else{
                        printf("mkfifo suscceess\n");
                }
        }

        return 0;
}

只读打开:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>

//int mkfifo(const char *pathname, mode_t mode);

int main()
{
        if((mkfifo("./file",0600) == -1) && errno != EEXIST ){
                printf("mkfifo failuer\n");
                perror("why");
        }
        int fd = open("./file",O_RDONLY);
        printf("open success\n");
        close(fd);
        return 0;
}

这样打开FIFO管道文件后,下一行open success不会执行,管道会阻塞,要有另一个程序以只写入的方式打开,才会接着执行

只写打开:


#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>

//int mkfifo(const char *pathname, mode_t mode);

int main()
{
        int fd = open("./file",O_WRONLY);
        printf("write open success\n");
        close(fd);
        return 0;
}

当只读打开FIFO文件程序运行后,只写打开FIFO文件程序在运行后,只读打开FIFO文件程序才会从阻塞状态中结束后接着运行

进程间通信_第3张图片

只读方式打开FIFO文件:

gcc read.c -o read

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

//int mkfifo(const char *pathname, mode_t mode);

int main()
{
        char buf[30] = {0};

        if((mkfifo("./file",0600) == -1) && errno != EEXIST ){
                printf("mkfifo failuer\n");
                perror("why");
        }
        int fd = open("./file",O_RDONLY);
        printf("open success\n");
        int n_read = 0;
        int cnt = 0;
        while(1){
                n_read = read(fd, buf, 30);
                printf("read %d byte from fifo, context = %s\n",n_read,buf);
                if(cnt == 5){
                        break;
                }
                cnt++;
        }
        close(fd);
        return 0;
}

只写方式打开FIFO文件:

gcc write.c -o write

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

//int mkfifo(const char *pathname, mode_t mode);

int main()
{
        int cnt = 0;
        char *str = "message from fifo!";
        int fd = open("./file",O_WRONLY);
        printf("write open success\n");
        while(1){
                write(fd, str, strlen(str));
                sleep(1);
                if(cnt == 5){
                        break;
                }
                cnt++;
        }
        close(fd);
        return 0;
}

这样就完成了基本的单向进程间的通信了

上述例子可以扩展成 客户进程—服务器进程 通信的实例,write_fifo的作用类似于客户端,可以打开多个客户端向一个服务器发送请求信息,read_fifo类似于服务器,它适时监控着FIFO的读端,当有数据时,读出并进行处理,但是有一个关键的问题是,每一个客户端必须预先知道服务器提供的FIFO接口,下图显示了这种安排:
进程间通信_第4张图片

三、消息队列

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。

可以做到进程间的相互发信息,而前面的无名,有名双工管道FIFO只能做单向操作。

1、特点

  1. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
  2. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
  3. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

操作思维示意:

A进程,B进程可以在linux内核中的链表消息队列中随意插入,读取数据

读取
写入
读取
写入
A进程
头数据
数据
数据
数据
数据
数据
B进程

2、原型

#include 
// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);
// 添加消息:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息队列:成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

在以下两种情况下,msgget将创建一个新的消息队列:

  • 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
  • key参数为IPC_PRIVATE。

函数msgrcv在读取消息队列时,type参数有下面几种情况:

  • type == 0,返回队列中的第一个消息;
  • type > 0,返回队列中消息类型为 type 的第一个消息;
  • type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。

可以看出,type值非 0 时用于以非先进先出次序读消息。也可以把 type 看做优先级的权值。(其他的参数解释,请自行Google之)

3、例子

服务端程序一直在等待特定类型的消息,当收到该类型的消息以后,发送另一种特定类型的消息作为反馈,客户端读取该反馈并打印出来。

开始接收信息端:

gcc msgGet.c -o get

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

//int msgget(key_t key, int msgflg);
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

typedef struct msgbuf {
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
}Msg;


int main()
{
        //获取
        Msg readBuf;
        int msgId = msgget(0x1234, IPC_CREAT|0777);
        if(msgId == -1){
                printf("get que failuer\n");
        }
        msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 888, 0);
        printf("read from que:%s\n",readBuf.mtext);

        Msg sendBuf = {988, "thank you for reach"};
        msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext), 0);

        return 0;
}

开始发送信息端:

gcc msgSend.c -o send

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

//int msgget(key_t key, int msgflg);
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

typedef struct msgbuf {
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
}Msg;


int main()
{
        //获取
        Msg sendBuf = {888, "this is message from quen"};
        int msgId = msgget(0x1234, IPC_CREAT|0777);
        if(msgId == -1){
                printf("get que failuer\n");
        }
        msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext), 0);

        Msg readBuf;
        msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 988, 0);
        printf("reaturn from get:%s\n",readBuf.mtext);

        return 0;
}

在这里插入图片描述

这样就基本完成了进程间通信,而且这两个进程跟消息队列是相互隔离的

关于key值的获取

头文件:

#include
#include

函数原型:

key_t ftok( const char * fname, int id )

fname就是你指定的文件名(已经存在的文件名),一般使用当前目录,如:

key_t key;
key = ftok(".", 1); 这样就是将fname设为当前目录。

id是子序号。虽然是int类型,但是只使用8bits(1-255)。

在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。

如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。

查询文件索引节点号的方法是: ls -i

当删除重建文件后,索引节点号由操作系统根据当时文件系统的使用情况分配,因此与原来不同,所以得到的索引节点号也不同。

如果要确保key_t值不变,要么确保ftok的文件不被删除,要么不用ftok,指定一个固定的key_t值,比如:

#define IPCKEY 0x111
char path[256];
sprintf( path, “%s/etc/config.ini”, (char*)getenv(“HOME”) );
msgid=ftok( path, IPCKEY );[/code]

同一段程序,用于保证两个不同用户下的两组相同程序获得互不干扰的IPC键值。

由于etc/config.ini(假定)为应用系统的关键配置文件,因此不存在被轻易删除的问题——即使被删,也会很快被发现并重建(此时应用系统也将被重启)。

ftok()的设计目的也在于此。

换索引减值key后的代码:

gcc msgGet.c -o get

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

//int msgget(key_t key, int msgflg);
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

typedef struct msgbuf {
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
}Msg;


int main()
{
        //获取
        Msg readBuf;
        key_t key;
        key = ftok(".",'z');
        printf("key = %x\n",key);//以16进制打出来

        int msgId = msgget(key, IPC_CREAT|0777);
        if(msgId == -1){
                printf("get que failuer\n");
        }
        msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 888, 0);
        printf("read from que:%s\n",readBuf.mtext);

        Msg sendBuf = {988, "thank you for reach"};
        msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext), 0);

        return 0;
}

gcc msgSend.c -o send

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

//int msgget(key_t key, int msgflg);
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

typedef struct msgbuf {
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
}Msg;


int main()
{
        //获取
        Msg sendBuf = {888, "this is message from quen"};
        key_t key;
        key = ftok(".",'z');
        printf("key = %x\n",key);//以16进制打出来

        int msgId = msgget(key, IPC_CREAT|0777);
        if(msgId == -1){
                printf("get que failuer\n");
        }

        msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext), 0);

        Msg readBuf;
        msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 988, 0);
        printf("reaturn from get:%s\n",readBuf.mtext);

        return 0;
}

运行结果:
执行结果

加了关闭消息队列的代码:

gcc msgGet.c -o get


#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

//int msgget(key_t key, int msgflg);
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

typedef struct msgbuf {
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
}Msg;


int main()
{
        //获取
        Msg readBuf;
        key_t key;
        key = ftok(".",'z');
        printf("key = %x\n",key);//以16进制打出来

        int msgId = msgget(key, IPC_CREAT|0777);
        if(msgId == -1){
                printf("get que failuer\n");
        }
        msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 888, 0);//读数据
        printf("read from que:%s\n",readBuf.mtext);

        Msg sendBuf = {988, "thank you for reach"};
        msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext), 0);//发数据

        msgctl(msgId, IPC_RMID, NULL);//控制关闭消息队列

        return 0;
}

gcc msgSend.c -o send

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

//int msgget(key_t key, int msgflg);
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

typedef struct msgbuf {
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
}Msg;


int main()
{
        //获取
        Msg sendBuf = {888, "this is message from quen"};
        key_t key;
        key = ftok(".",'z');
        printf("key = %x\n",key);//以16进制打出来

        int msgId = msgget(key, IPC_CREAT|0777);
        if(msgId == -1){
                printf("get que failuer\n");
        }

        msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext), 0);//发数据
        printf("send ovre!\n");
        Msg readBuf;
        msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 988, 0);//读数据
        printf("reaturn from get:%s\n",readBuf.mtext);

        msgctl(msgId, IPC_RMID, NULL);//控制关闭消息队列

        return 0;
}

进程间通信_第5张图片

IPC_RMID 关闭内核中msgId的ID消息队列

四、共享内存

共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。

1、特点

  1. 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
  2. 因为多个进程可以同时操作,所以需要进行同步。
  3. 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

2、原型

#include 
// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int flag);
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr);
// 控制共享内存的相关信息:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
  • 当用shmget函数创建一段共享内存时,必须指定其 size;而如果引用一个已存在的共享内存,则将 size 指定为0 。

  • 当一段共享内存被创建以后,它并不能被任何进程访问。必须使用shmat函数连接该共享内存到当前进程的地址空间,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。

  • shmdt函数是用来断开shmat建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。

  • shmctl函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。

共享内存使用案例:

写入共享内存

gcc shmw.c -o w

#include <sys/types.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>

//int shmctl(int shmid, int cmd, struct shmid_ds *buf);   //删除共享内存

//void *shmat(int shmid, const void *shmaddr, int shmflg);  //把共享内存映射到进程

//int shmdt(const void *shmaddr);    //释放掉共享内存,就把进程里连接共享内存的连接断开,但系统中共享内存还在

//int shmget(key_t key, size_t size, int shmflg);    //删除系统中的共享内存

int main()
{
        int shmid;
        char *shmaddr;
        key_t key;
        key = ftok(".", 1);

        shmid = shmget(key, 1024*4, IPC_CREAT|0666);  //创建/打开共享内存
        if(shmid == -1){
                printf("shmget no ok!\n");
                exit(-1);
        }

        shmaddr = shmat(shmid, 0, 0);    //挂载共享内存

        printf("shmat ok!\n");

        strcpy(shmaddr, "yangyingchu");   //操作共享内存,写入共享内存数据

        sleep(5);   //等待时间,等待其它进程把数据读走

        shmdt(shmaddr);   //断开共享内存的连接

        shmctl(shmid, IPC_RMID, 0);  //删除共享内存

        printf("quit\n");

        return 0;

}

读取共享内存数据

gcc shmr.c -o r

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>

//int shmctl(int shmid, int cmd, struct shmid_ds *buf);   //删除共享内存

//void *shmat(int shmid, const void *shmaddr, int shmflg);  //把共享内存映射到进程

//int shmdt(const void *shmaddr);    //释放掉共享内存,就把进程里连接共享内存的连接断开,但系统中共享内存还在

//int shmget(key_t key, size_t size, int shmflg);    //删除系统中的共享内存

int main()
{
        int shmid;
        char *shmaddr;
        key_t key;
        key = ftok(".", 1);

        shmid = shmget(key, 1024*4, 0);  //创建/打开共享内存
        if(shmid == -1){
                printf("shmget no ok!\n");
                exit(-1);
        }

        shmaddr = shmat(shmid, 0, 0);    //挂载共享内存

        printf("shmat ok!\n");

        printf("data: %s",shmaddr);   //操作共享内存,读共享内存数据

        shmdt(shmaddr);   //断开共享内存的连接


        printf("quit\n");

        return 0;

}

执行结果

ipcs -m
查看系统中的共享内存

ipcrm -m 32782
删除系统中共享内存ID为32782的共享内存

五、信号(signal)2

对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号,为 Linux 提供了一种处理异步事件的方法。比如,终端用户输入了 ctrl+c 来中断程序,会通过信号机制停止一个程序。

  1. 信号的名字和编号:
    • 每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。
    • 信号定义在signal.h头文件中,信号名都定义为正整数。
    • 具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。

进程间通信_第6张图片

  1. 信号的处理:
    信号的处理有三种方法,分别是:忽略、捕捉和默认动作

    • 忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL和SIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景
    • 捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
    • 系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。
      具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。在此,我就不详细展开了,需要查看的,可以自行查看。也可以参考 《UNIX 环境高级编程(第三部)》的 P251——P256中间对于每个信号有详细的说明。

信号使用:

其实对于常用的 kill 命令就是一个发送信号的工具,kill 9 PID来杀死进程。比如,我在后台运行了一个 top 工具,通过 ps 命令可以查看他的 PID,通过 kill 9 来发送了一个终止进程的信号来结束了 top 进程。如果查看信号编号和名称,可以发现9对应的是 9) SIGKILL,正是杀死该进程的信号。而以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程。

kill -9 ID

kill -SIGKILL ID
信号杀死进程

对于信号来说,最大的意义不是为了杀死进程,而是为了实现一些异步通讯的手段,即信号的三种使用方法中的捕捉信号

总结:

  • 信号对linux系统来说是一个很重要的异步通信的方法

  • 比如按 ctrl+c 能来中段程序,就是发送了一个信号

  • kill -l 来查看系统中的信号

  • 信号一般的处理方式为:忽略、捕捉和默认动作

    • 忽略就是收到信号不管它
    • 捕捉是根据相对应配置的函数处理信号
    • 默认动作是根据shll指令调用系统默认的处理方式处理信号
  • SIGKILL和SIGSTOP这两个信号不能被忽略

  • kill -9 ID、kill -SIGKILL ID来操控信号

信号处理函数的注册

入门版:函数signal
高级版:函数sigaction

信号处理发送函数:

1.入门版:kill
2.高级版:sigqueue


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

//typedef void (*sighandler_t)(int);

//sighandler_t signal(int signum, sighandler_t handler);

void handler(int signum)
{
        printf("get signum= %d\n",signum);
        printf("never quit\n");
}

int main()
{
        signal(SIGINT, handler);    // 当接收到 ctrl+c 信号后传给 handler 函数处理
        while(1);
        return 0;
}

此进程用 Ctrl + C 不能终止要用 kill -9 ID 来杀死进程

处理其它信号:

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

//typedef void (*sighandler_t)(int);

//sighandler_t signal(int signum, sighandler_t handler);

void handler(int signum)
{
        printf("get signum= %d\n",signum);
        switch(signum){
                case 2:
                        printf("SIGINT\n"); //处理Ctrl+C指令
                        break;
                case 9:
                        printf("SIGKILL\n"); //处理不了,系统会规避
                        break;
                case 10:
                        printf("SIGUSR1\n");
                        break;
        }

        printf("never quit\n");
}

int main()
{
        signal(SIGINT, handler);    // 捕捉ctrl+c信号
        signal(SIGKILL, handler);   //捕捉kill指令信号
        signal(SIGUSR1, handler);    //发指令 kill -10 ID
        while(1);
        return 0;
}

用程序捕捉信号,执行信号动作

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

//int kill(pid_t pid, int sig);

int main(int argc, char **argv)
{
        int signum;
        int pid;

        signum = atoi(argv[1]);
        pid = atoi(argv[2]);

        kill(pid, signum);
        printf("signum kill ok!\n");

        return 0;
}

用system函数执行:

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

//int kill(pid_t pid, int sig);

int main(int argc, char **argv)
{
        int signum;
        int pid;
        char cmd[128] = {0};


        signum = atoi(argv[1]);
        pid = atoi(argv[2]);

        sprintf(cmd, "kill -%d %d",signum, pid);  //配置指令

        system(cmd);  //执行指令

        printf("signum kill ok!\n");

        return 0;
}

忽略信号:

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

//typedef void (*sighandler_t)(int);

//sighandler_t signal(int signum, sighandler_t handler);

void handler(int signum)
{
        printf("get signum= %d\n",signum);
        switch(signum){
                case 2:
                        printf("SIGINT\n");
                        break;
                case 9:
                        printf("SIGKILL\n");
                        break;
                case 10:
                        printf("SIGUSR1\n");
                        break;
        }

        printf("never quit\n");
}

int main()
{
        signal(SIGINT, SIG_IGN);    // 捕捉 ctrl+c信号
        signal(SIGKILL, handler);
        signal(SIGUSR1, handler);
        while(1);
        return 0;
}


SIG_IGN 忽略信号

初级:

发信号:kill函数
控制:signal函数

重点放在了动作上

高级版:

发信号:sigqueue函数
控制:sigaction函数

用什么发
怎么放入消息
怎么绑定函数
如何读出数据
信号
携带操作信息
忽略信号
能够额外接收数据
结构体
谁发的
想要的数据
想要的结构体数据
结构体数据
结构体数据
指针为空
指针为非空
处理信号时阻塞的作用
收数据指定SA_SIGINFO
备份原有的信号操作不用可以写NULL
发信号
sigqueue
信号动作
消息
int
char
收信号
sigaction
int signum
struct sigaction *act
*p1
*p2
num
siginfo_t
pid
si_int
si_value
int
char *p
*p
无数据
有数据
mask
int flags
struct sigaction *oldact

sigqueue 的发送函数原型:

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

sigaction 的处理函数原型:

#include 
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只能任选其一

siginfo_t 结构体:

 siginfo_t {
               int      si_signo;    /* Signal number */
               int      si_errno;    /* An errno value */
               int      si_code;     /* Signal code */
               int      si_trapno;   /* Trap number that caused
                                        hardware-generated signal
                                        (unused on most architectures) */
               pid_t    si_pid;      /* Sending process ID */
               uid_t    si_uid;      /* Real user ID of sending process */
               int      si_status;   /* Exit value or signal */
               clock_t  si_utime;    /* User time consumed */
               clock_t  si_stime;    /* System time consumed */
               sigval_t si_value;    /* Signal value */
               int      si_int;      /* POSIX.1b signal */
               void    *si_ptr;      /* POSIX.1b signal */
               int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
               int      si_timerid;  /* Timer ID; POSIX.1b timers */
               void    *si_addr;     /* Memory location which caused fault */
               int      si_band;     /* Band event */
               int      si_fd;       /* File descriptor */
}

si_value结构体:

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

信号携带信息:

发整数

接收读端:

gcc new.c -o new

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

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

//void (*sa_sigaction)(int, siginfo_t *, void *);

void handler(int signum, siginfo_t *info, void *context)
{
        printf("get signum %d\n",signum);
        if(context != NULL){
                printf("get data = %d\n",info->si_int);
                printf("get data = %d\n",info->si_value.sival_int);
                printf("get from pid = %d\n",info->si_pid);
        }
}

int main()
{
        struct sigaction act;
        printf("pid = %d\n",getpid());
        act.sa_sigaction = handler;
        act.sa_flags = SA_SIGINFO;  //be able to get message
        sigaction(SIGUSR1, &act, NULL);
        while(1);
        return 0;
}

发信息端:

gcc send.c -o send

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

//int sigqueue(pid_t pid, int sig, const union sigval value);

int main(int argc, char **argv)
{
        int signum = atoi(argv[1]);
        int pid = atoi(argv[2]);
        union sigval value;
        value.sival_int = 100;

        sigqueue(pid, signum, value);

        printf("pid = %d done!\n",getpid());
        return 0;
}

发送字符串:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/wait.h>

void sig_action(int signo, siginfo_t *info, void *addr)
  {
     if (signo == SIGUSR1)
      printf ("the signo is : SIGUSR1 \n");
     printf("%s\n", (char*)info->si_value.sival_ptr);       //打印随着信号一起传递过来的字符串
  }

int main(int argc, char* const argv[])
{
   struct sigaction act;
   act.sa_sigaction = sig_action;   //定义接受信号的处理函数
   act.sa_flags = SA_SIGINFO;    //设置该参数表示可以接收额外的参数;
   sigaction(SIGUSR1, &act, NULL);    //这个是信号的接受函数
   pid_t pid;
   if ((pid = fork()) == -1)
       printf ("fault fork \n");
   else if (pid == 0)
    {
       union sigval val;
       val.sival_ptr = "hello world";   //把字符串发在 sigqueue函数的第三个参数中会在信号发送时整合到在sigqueue函数的>第二个参数中,一起发送给接受信号的进程
       sigqueue(getppid(), SIGUSR1, val);    //这个是信号的发送函数
     }
   while (((pid = wait(NULL)) == -1 && errno == EINTR) || pid > 0)  {}

   return 0;
}

发送字符串只能在共享内存或者同一进程下才可以发送

六、信号量

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

1、特点

  1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
  2. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
  3. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
  4. 支持信号量组。

2、原型

  1. 最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。
  2. Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
#include 
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);

当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。

在semop函数中,sembuf结构的定义如下:

struct sembuf
{
    short sem_num; // 信号量组中对应的序号,0~sem_nums-1
    short sem_op;  // 信号量值在一次操作中的改变量
    short sem_flg; // IPC_NOWAIT, SEM_UNDO
}

其中 sem_op 是一次操作中的信号量的改变量:

  • 若sem_op > 0,表示进程释放相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则换行它们。

  • 若sem_op < 0,请求 sem_op 的绝对值的资源。

    • 如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。
    • 当相应的资源数不能满足请求时,这个操作与sem_flg有关。
      • sem_flg 指定IPC_NOWAIT,则semop函数出错返回EAGAIN。
      • sem_flg 没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
        • 当相应的资源数可以满足请求,此信号量的semncnt值减1,该信号量的值减去sem_op的绝对值。成功返回;
        • 此信号量被删除,函数smeop出错返回EIDRM;
        • 进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt值减1,函数semop出错返回EINTR
  • 若sem_op == 0,进程阻塞直到信号量的相应值为0:

    • 当信号量已经为0,函数立即返回。
    • 如果信号量的值不为0,则依据sem_flg决定函数动作:
      • sem_flg指定IPC_NOWAIT,则出错返回EAGAIN。
      • sem_flg没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
        • 信号量值为0,将信号量的semzcnt的值减1,函数semop成功返回;
        • 此信号量被删除,函数smeop出错返回EIDRM;
        • 进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR

在semctl函数中的命令有多种,这里就说两个常用的:

  • SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
  • IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。

控制信号量的基础框架:

#include <sys/ipc.h>
#include <sys/sem.h>

//int semget(key_t key, int nsems, int semflg);

//int semctl(int semid, int semnum, int cmd, ...);

union semun {
        int              val;    /* Value for SETVAL */
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
        unsigned short  *array;  /* Array for GETALL, SETALL */
        struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                    (Linux-specific) */
};


int main(int argc, char **argv)
{
        key_t key = ftok(".",2);
        int semid;
                        //key 信号量集合中有一个信号量
        semid = semget(key, 1, IPC_CREAT|0666);  //获取信号量,或者创建信号量

        union semun initsem;
        initsem.val = 1;

                        //0 代表操作第0个信号量
        semctl(semid, 0, SETVAL, initsem);  //初始化信号量
                        //SETVAL 设置信号量的值,设置为:initsem

        int pid = fork();
        if(pid > 0){
                //去拿锁
                printf("this is father!\n");
                //锁放回去
        }
        else if(pid == 0){
                printf("this is child\n");
        }
        else{
                printf("fork error\n");
        }
        return 0;
}

信号量加锁后:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

//int semget(key_t key, int nsems, int semflg);

//int semctl(int semid, int semnum, int cmd, ...);

//int semop(int semid, struct sembuf *sops, size_t nsops);

union semun {
        int              val;    /* Value for SETVAL */
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
        unsigned short  *array;  /* Array for GETALL, SETALL */
        struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                    (Linux-specific) */
};

void pGetKey(int semid)
{
        struct sembuf sops;
        sops.sem_num = 0;        /* Operate on semaphore 0 */
        sops.sem_op = -1;         /* Wait for value to equal 0 */
        sops.sem_flg = SEM_UNDO;

        if (semop(semid, &sops, 1) == -1) {
                perror("semop p");
                exit(-1);
        }
        printf("getkey!\n");

}

void vPutBackKey(int semid)
{
        struct sembuf sops;
        sops.sem_num = 0;        /* Operate on semaphore 0 */
        sops.sem_op = 1;         /* Wait for value to equal 0 */
        sops.sem_flg = SEM_UNDO;

        if (semop(semid, &sops, 1) == -1) {
                perror("semop v");
                exit(-1);
        }
        printf("put back the key!\n");

}


int main(int argc, char **argv)
{
        key_t key = ftok(".",2);
        int semid;
                        //key 信号量集合中有一个信号量
        semid = semget(key, 1, IPC_CREAT|0666);  //获取信号量,或者创建信号量

        union semun initsem;
        initsem.val = 0;
    
                        //0 代表操作第0个信号量
        semctl(semid, 0, SETVAL, initsem);  //初始化信号量
                        //SETVAL 设置信号量的值,设置为:initsem

        int pid = fork();
        if(pid > 0){ 
                //去拿锁
                pGetKey(semid);
                printf("this is father!\n");
                vPutBackKey(semid);
                //锁放回去
                semctl(semid, 0, IPC_RMID, initsem);
                //销毁信号量集
        }
        else if(pid == 0){ 
                //pGetKey(semid);
                printf("this is child\n");
                vPutBackKey(semid);
        }
        else{
                printf("fork error\n");
        }
        return 0;
}       

执行结果:

this is child
put back the key!
getkey!
this is father!
put back the key!

  1. 进程间通信参考链接 ↩︎

  2. 信号参考链接 ↩︎

你可能感兴趣的:(linux,网络)