操作系统之进程间通信方式

进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,Inter Process Communication)

由上表我们可以看出IPC根据功能可以分三类:

通信:进程间的信息交互;

同步:进程或是线程之间的同步;

信号:信号是事件发生时对进程的通知机制。有时也称之为软件中断。打断了程序执行的正常流程。

按《UNIX环境高级编程(第二版)》P421所述,经典IPC为:管道(包括无名管道pipe和命名管道FIFO)、消息队列、共享内存、信号量;按传智播客视频所述:通信方式:(无名)管道(pipe) 、命名管道(FIFO)、内存映射(mmap)、信号(signal)、 本地套接字(domain);同步方式:信号量(semaphore)、文件锁;

 

一、管道

1、无名管道pipe

无名管道的实现原理管道实为内核使用 环形队列 机制(以便管道被循环利用),借助内核缓冲区(4K=512bytes * 8)实现
无名管道的特点属于半双工通信,只能作用于具有血缘 系(父子进程和兄弟进程)的进程之间 的数据传递
管道的一端连接一个进程的输出(向管道中放入信息)。管道的另一端连接一个进程的输入(取出管道的信息)。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当两个进程都终结的时候,管道也自动消失。
管道 特质
1、 管道 属于Linux7种文件类型之一,是 文件 (实为 内核缓冲区 ,不占用磁盘存储;
2、由两个文件描述符引用,一个表示读端,一个表示写端.
3、规定数据从管道的 写端流入管道,从读端流出 (单向流动)在使用管道时是不存在消息或是消息边界的概念,从管道中读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是什么。
管道的局限性
1、数据自己读不能自己写。
2、数据一旦被读走, 便不在管道中存在, 不可重复读取
3、由于管道采用 半双工通信 方式,因此,数据只能在一个方向上流动 。(现在某些系统提供全双工管道)
4、 只能在有公共祖先的进程间使用管道
应用:
每当你在管道线中键入一个由shell执行的命令序列时,shell为每一条命令单独创建一进程,然后将前一条命令进程的标准输出用管道与后一条命令的标准输入相连接。
 

pipe函数调用pipe 系统函数即可创建一个管道。

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

返回值:

参数pipefd[2]:
    要求文件描述符数组,无需open但需手动关闭,
    规定:fd[0]用来读,fd[1]用于写,(类 PCB中的标准输入fd[0]、输出文件描述符fd[1],默认是打开的)

pipe半双工通信实例如下:

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

int main()
{
        int fd[2];//读写两个文件描述符
        int ret = pipe(fd);//创建管道
        pid_t pid;
        char buf[1024];
        char *str = "hello pipe test\n";
        if(ret == -1){
                perror("pipe error");
                exit(1);
        }
        pid = fork();//创建子进程,,由 fork 机制建立,,
                    //因此就只能作用于具有血缘关系的父子进程和兄弟进程之间的通信
                    // 规定数据的流动方向只能是:管道的写端流入,从读端流出(单向流动);
        if(pid == -1){
                perror("fork error");
                exit(1);
        }else if(pid == 0){//子进程
                close( fd[1] );//关闭标准输出,也即关闭写端
                ret = read(fd[0], buf, sizeof(buf));
                if(ret == 0)
                        printf("read end\n");
                write(STDOUT_FILENO, buf, ret);
        }else{//父进程
                close( fd[0] );//关闭标准输入,也即关闭读端
                write(fd[1], str, strlen(str));
        }
        return 0;
}

两个管道实现全双工进程通信:

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

void child_rw_pipe(int readfd, int writefd){
    char *message = "This is the message1, from child";
    write(writefd, message, strlen(message)+1);
    char message2[100];
    read(readfd, message2, 100);
    printf("Child read the message :%s \n", message2);
}
void parent_rw_pipe(int readfd, int writefd){
    char *message = "This is the message2, from parent";
    write(writefd, message, strlen(message)+1);
    char message2[100];
    read(readfd, message2, 100);
    printf("parent read the message :%s \n", message2);
}
int main(void)
{
    int pipe1[2], pipe2[2];
    pid_t pid;
    int stat_val;

    printf("realize full-duplex communication.\n");
    /*创建管道*/
    if(pipe(pipe1)){
        printf("Create pipe1 failed!\n");
        exit(1);
    }
    if(pipe(pipe2)){
        printf("Create pipe2 failed!\n");
        exit(1);
    }
    pid = fork();// 管道是由 fork 机制建立,,
                // 因此就只能作用于具有血缘关系的父子进程和兄弟进程之间的通信

    if(pid < 0){
        printf("Pid is error.\n");
        exit(1);
    }else if(pid == 0){ //子进程
        close(pipe1[1]); // 关闭写
        close(pipe2[0]); // 关闭读
        child_rw_pipe(pipe1[0], pipe2[1]);
    }else if(pid > 0){ // 父进程
        close(pipe1[0]); // 关闭读
        close(pipe2[1]); // 关闭写
        parent_rw_pipe(pipe2[0], pipe1[1]);
        wait(&stat_val);
        exit(0);
    }
    return 0;
}

同样也可以由管道实现父子进程之间的同步:

标准VO库提供了两个函数popen和pclose。这两个函数实现的操作是:创建一个管道,调用fork产生一个子进程,关闭管道的不使用端,执行一个she11以运行命令,然后等待命令终止。

popen函数  
#include <stdio.h>
FILE* popen(congt char*cmdstring,const char*type);

返回值:
    若成功则返回文件指针出错返回NULL
参数:
    type是“r”,则文件指针连接到cmdstring的标准输出;
      type是“w”,则文件指针连接到cmdstring的标准输入;

pclose函数:

#include <stdio.h>
int pclose(FILE*p);

2、FIFO(命名管道)

FIFO的实现原理:FIFO被称为 命名管道,利用文件系统为管道命名。是一种特殊的文件类型,它 通过文件的 路径进行关系绑定 ,当一个进程以读方式 打开该文件 ,而另一个进程以写方式打开该文件,那么内核就会为这两个进程建立管道
FIFO的特点:FIFO也是一种半双工的通信方式,管道本质上是一个 先进先出的队列数据结构。也即先写入的数据被先读出来。 可以用于 任意进程间通信(相比之下无名管道亦称为管道只能用于父子进程间的通信)
FIFO的打开规则
1.如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;
否则,可能阻塞直到有相应进程为写而打开该FIFO(若当前打开操作设置了阻塞标志);或者,成功返回(若当前为读而打开的操作没有设置阻塞标志)。
2.如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(若当前打开操作设置了阻塞标志):或者,返回ENXIO错误(若当前打开操作没有设置阻塞标志)。
总之, 一旦设置了阻塞标志,调用mkfifo建立好之后,那么管道的两端读写必须分别打开,有任何一方未打开,则在调用open的时候就阻塞

mkfifo函数:创建命名管道;

#include<sys/stat.h>
int mkfifo( const char* path, mode_t mode );

返回:
    0:成功
    -1:出错,并设定errno
参数:
    path: 要用于FIFO文件的路径名,存在于文件系统中的;
    mode: 文件权限,与open函数相同;

为写打开FIIO实例:

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

int main(int argc,char *argv[])
{
    if(argc < 2){ // 参数个数
        printf("please input format is: ./a.out fifo_name\n");
        exit(1);
    }

    // 创建有名管道
    int ret = access(argv[1],F_OK);//判断文件是否存在
    if(ret == -1){
        int r = mkfifo(argv[1],0664);  //FIFO文件的路径名, 权限;
        if(r == -1){
            perror("mkfifo error");
            exit(1);
        }
        printf("有名管道%s创建成功\n",argv[1]);
    }

    // 打开文件
    int fd = open(argv[1],O_WRONLY);
    if(fd == -1){
        perror("open error");
        exit(1);
    }

    // 写入内容
    char *p = "hello world!";
    while(1){
        sleep(1);
        int len = write(fd,p,strlen(p) + 1);
    }

    close(fd);
    return 0;
}
注:
使用 sleep()(存在于由父进程所执行的代码中),意在允许子进程先于父进程获得系统调度并使用 CPU,
以便在父进程继续运行之前完成自身任务并退出。要想确保这一结果, sleep()的这种用法并非万无一失,
可以用信号间的对同步实现

为读打开FIIO实例:

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

int main(int argc,char *argv[])
{
    if(argc < 2){
        printf("please input format is: ./a.out fifo_name\n");
        exit(1);
    }
    // 创建有名管道
    int ret = access(argv[1],F_OK);//判断文件是否存在
    if(ret == -1)
    {
        int r = mkfifo(argv[1],0664);
        if(r == -1){
            perror("mkfifo error");
            exit(1);
        }
        printf("有名管道%s创建成功\n",argv[1]);
    }

    int fd = open(argv[1],O_WRONLY);
    if(fd == -1){
        perror("open error");
        exit(1);
    }

    // 读取内容
    char buf[512];
    while(1){
        int len = read(fd,p,strlen(p) + 1);
        buf[len] = 0;
        printf("buf = %s\n,len = %d",buf,len);
    }
    close(fd);
    return 0;
}

 

二、消息队列

此处讲的是 S ystem V IPC  消息队列 ,内容可见《UNIX环境高级编程(第二版)》P432 和 《Linux_UNIX系统编程手册上下册》 P814。 而非POSIX 消息队列; 操作系统得知识博大精深可不是一篇笔记可以描述的,还是得 多学习 多看书多实践哦!!!
 
消息队列是消息的链接表,存放在内核中并由消息队列标识符标识。其标识符为队列ID( queue ID 。可以把消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向其中按照一定的规则添加新消息; 消息总是放在队列尾端。对消息队列有读权限的进程则可以从消息队列中读走消息。
每个消息都由三部分组成,它们是:正长整型类型字段、非负长度(nbytes)以及实际数据字节(对应于长度)。
 
 
msgget()系统调用:用户创建一个新的队列或是打开一个现存的队列。
#include<sys/types.h>
#include<sys/msg.h>
int msgget(key_t key,int msgflg);

返回值:
    找到则返回对象表示符;
    如果没有找到匹配的队列并且在 msgflg 中指定了IPC_CREAT,那么就会创建一个新队列并返回该队列的标识符。出错返回 -1
参数:
    key参数是一个键;
   msgflg 参数是一个指定施加于新消息队列之上的权限或检查一个既有队列的权限的位掩码。 
msgsnd()系统调用:向消息队列写入一条消息;
 

#include<sys/types.h>
#include<sys/msg.h>
int msgsnd(int msqid,const void *msgp, size_t msgsz,int msgflg);

返回值:成功返回 0,错误返回-1
参数:
    第一个参数是消息队列标识符( msqid)
    第二个参数 msgp 是结构指针,用于存放被发送或接收的消息
    msgsz 参数指定了msgp字段中包含的字节数
    msgflg为消息标志

msgrcv()系统调用:从消息队列中读取(以及删除)一条消息并将其内容复制进 msgp 指向的缓冲区中。

#include<sys/types.h>
#include<sys/msg.h>
ssizet msgrcv(int msqid,void *msgp,sizet maxmsgsz,long msgtyp,int msgflg);

返回值:成功返回数据长度,错误返回-1

参数 
    msqid:消息队列的标识码。
    *msgp:指向消息缓冲区的指针。
    msgsz:消息的长短
    参数 msgflg:标志位
msgctl() 系统调用:在标识符为 msqid 的消息队列上执行控制操作(比如删除...等)。
#include<sys/types.h>/*For portability*/
#include<sys/msg.h>
int msgctl(int msgid,int cmd,struct msqid_ds *buyf);

实现消息队列代码如下:

msg_send.c

#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/msg.h>
#include<errno.h>

#define MAX_TEXT 512
struct msg_st{
    long int msg_type;
    char text[MAX_TEXT];
};
int main(void)
{
    int running = 1;
    struct msg_st data;
    char buffer[BUFSIZ];
    int msgid = -1;
    
    msgid = msgget((key_t)1234, 0666 | IPC_CREAT);//建立消息队列
    if(msgid==-1){
        fprintf(stderr, "msgget failed with error:sd\n", errno);
        exit(EXIT FATLURE);
    }    
    while(running)//向消息队列中写消息,直到写入结束标志:end
    {
        printf("Enter text:");
        fgets(buffer, BUFSIZ, stdin);//输入数据
        data.msg_type = 1;
        strcpy(data.text, buffer);
        if( msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1 ){//向队列发送数据
            fprintf(stderr,"magsnd failed\n");
            exit(EXIT_FAILURE);
        }
        if(strncmp(buffer,"end",3)==0)
            running = 0;//输入end结束输入
        sleep(1);
    }
    exit(EXIT_SUCCESS);
}

msg_receive.c

#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/msg.h>
#include<errno.h>

struct msg_st{
    long int msg_type;
    char text[BUFSIZ];
};
int main(void)
{
    int running = 1;
    struct msg_st data;
    int msgid = -1;
    long int msgtype = 0;
    
    msgid = msgget((key_t)1234, 0666 | IPC_CREAT);//建立消息队列
    if(msgid==-1){
        fprintf(stderr, "msgget failed with error:sd\n", errno);
        exit(EXIT FATLURE);
    }    
    while(running)//向消息队列中获取消息,直到读到结束标志:end
    {
        if( msgrcv(msgid, (void*)&data, BUFSIZ, 0) == -1 ){//从消息队列中读取数据
            fprintf(stderr,"magsnd failed\n");
            exit(EXIT_FAILURE);
        }
        printf("Your wrote: %s\n", data.text );
        if(strncmp(data.text,"end",3) ==0 )
            running = 0;//输入end结束输入
    }
    if( msgctl(msgid, IPC_RMID, 0) == -1){ // 删除消息队列
        fprintf(stderr, "msgctl error:sd\n");
        exit(EXIT FATLURE);
    }
    exit(EXIT_SUCCESS);
}

System V IPC 补充

System V IPC key 是一个整数值,其数据类型为 key_t。 IPC get 调用将一个 key 转换成相应的整数 IPC 标识符。这些调用能够确保如果创建的是一个新 IPC 对象,那么对象能够得到一个唯一的标识符,如果指定了一个既有对象的 key,那么总是会取得该对象的同样的标识符。

如何产生唯一的 key 呢?

1、在创建 IPC 对象的 get 调用中将 IPC_PRIVATE 常量作为 key 的值, 这样就会导致每个调用都会创建一个全新的 IPC 对象,从而确保每个对象都拥有一个唯一的 key

2、使用 ftok( ) 函数生成一个(接近唯一) key

ipcs 命令

ipcs 命令是 System V IPC 领域中类似于 ls 文件命令的命令。 使用 ipcs 能够获取系统上 IPC 对象的信息。在默认情况下, ipcs 会显示出所有对象。

 

ipcrm 命令:

ipcrm 命令是 System V IPC 领域中类似于 rm 文件命令的命令。用于删除一个 IPC 对象。

ipcrm -X key

incrm -x id

在上面给出的命令中既可以将一个 IPC 对象的 key 指定为参数 key,也可以将一个 IPC 对象的标识符指定为参数 id 

并且使用小写的 x 替换其大写形式或使用小写的 q(用于消息队列)或 s(用于信号量)或 m(用于共享内存)

System V IPC 补充结束

 

三、信号量

此处讲的是 POSIX 信号量 ,内容可见《UNIX环境高级编程(第二版)》P436 和 《Linux_UNIX系统编程手册上下册》 P940。 而非System V 信号量; 操作系统得知识博大精深可不是一篇笔记可以描述的,还是得 多学习 多看书多实践哦!!!
 
信号量实际上是同步原语而不是IPC,匿名信号量是可以用于进程间同步亦可以用于线程间同步。可见线程同步信号量章节的笔记。

 

四、共享内存

此处讲的是 S ystem V IPC  共享内存 ,内容可见《UNIX环境高级编程(第二版)》P441和 《Linux_UNIX系统编程手册上下册》 P863。 而非POSIX 共享内存; 操作系统得知识博大精深可不是一篇笔记可以描述的,还是得 多学习 多看书多实践哦!!!
共享内存可以说是最有用的进程间通信方式,也是效率最快的IPC形式。是针对其他通信机制运行效率较低而设计的。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。                                                                  
由于多个进程共享同一块内存区域,必然需要某种同步机制,使用互斥锁和信号量都可以

4.1、内存映射分类

调用系统函数mmap()的进程会在其虚拟地址空间中创建一个新的内存映射;
映射分为两类:文件映射、匿名映射;
文件映射:将文件的部分区域映射入调用进程的虚拟内存。映射一旦完成,对文件映射内容的访问则转化为对相应内存区域的字节操作。映射页面会按需自动从文件中加载。
匿名映射: 其映射页面的内容会被初始化为0(可以把它看作是一个内容总是被初始化为0的虚拟文件的映射)当两个或是更多个进程共享相同分页的时候,每个进程都有可能看到其他进程堆分页内容做出的变更,这取决于映射的类型:私有映射、共享映射;
映射也还可分为两类:私有映射、共享映射;
私有映射MAP_SHARED):映射内容发生改变其他进程不可见,也即对本进程是私有的;内核使用“写时复制”实现;
共享映射MAP_PRIVATE):映射内容发生改变其他进程均可见;
私有文件映射:
私有匿名映射:
共享文件映射:
共享匿名映射:
达成共享映射的方式:1、两个进程(无血缘关系)都指定同一个文件的相同部分进行映射;2、由fork()创建子进程自父进程处实现 继承映射;
 
mmap( )系统调用:在调用进程的虚拟地址空间中创建一个新映射。
# include<sys/mman.h>
void * mmap(void * addr, sizet length, int prot, int flags, int fd, off_t ofset);

返回:
   成功,返回创建映射区首地址;
   失败,返回MAP_FAILED 宏
参数:
    addr: 建立映射区的首地址,为NULL,由Linux内核指定映射地址;非NULL,则从指定位置开始;
    length:欲创建映射区的大小
    prot:  映射区权限 PROT_READ、PROT_WRITE、PROT_READ | PROT_WRITE ;
            (可读可写可执行......)
    flags: 标志位参数(常用于设定更新物理区域、设置共享、创建匪名映射区)
        MAP_SHARED:所做的修改其他进程不可见,会将映射区所做的操作反映到磁盘上;
        MAP_PRIVATE:所做的修改其他进程不可见,映射区所做的修改不会反映到物理设备,
        MAP_ANONYMOUS (或MAP_ANON宏:它是Linux操作系统特有的宏):匿名映射的标志;
                      若要使用匿名映射,flags = MAP_ANON 且 fd = -1
    fd:用来建立映射区的文件描述符
    offset:映射文件的相对于映射内存的偏移量(k 的整数倍)

mmap( )调用操作页。addr和offset参数都必须按页大小(默认4KB)对齐。
        也就是说,它们必须是页大小的整数倍。

munmap( )系统调用:执行与 mmap()相反的操作,即从调用进程的虚拟地址空间中删除一个映射

# include<sys/mman.h>
int munmap(void * addr, size t length);

返回:
    成功:0;
    失败:-1;
参数
    addr: mmap的返回值
    length: 映射区大小

mmap和munmap注意事项:

1.映射区建立过程中隐含一次读操作

2.MAP_SHARED时映射区权限 <= 打开文件权限

3.映射区建立成功,文件即可关闭.

4.大小为0的文件无法创建映射区

5.munmap参数应与mmap返回值严格对应

6.偏移位置必须为4K的整数倍

7.mmap返回值判断不能省

5.2 文件映射

将文件的部分区域映射入调用进程的虚拟内存。映射一旦完成,对文件映射内容的访问则转化为对相应内存区域的字节操作。映射页面会按需自动从文件中加载。

5.2.1、私有文件映射

此内容待补。。。
 

5.2.2、共享文件映射

共享文件映射存在两个用途:内存映射 I/O 和 IPC。
 
1、 使用共享文件映射的 IPC
  • fork后父子进程由于 共享文件描述符,可以实现父子进程通信,已由前述实现;
  • 通过创建映射区, f ork后父子进程将共享映射区,实现父子进程通信;
  • 非血缘关系间的进程使用同一文件创建映射区通信实例:

2、使用共享文件映射的内存映射IO

存储映射I/O(Memory-mappedI/O)使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,相应字节就自动地写入文件。这样就可以在不使用read和write的情况下执行I/O,也即代替代替了read和write。

5.3 匿名映射

使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。可以直接使用匿名映射来代替,其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。

匿名映射的创建方法:

在调用mmap系统调用的时候设置对应的flags标志位和文件描述符fd;如:

int *p = mmap(NULL, 4, PROT_READIPROT_WRITE, MAP_SHAREDIMAP_ANONYMOUS, -1, 0);
 // “4”为指定映射区的大小;

注:在使用匿名映射的时候,flags = MAP_SHAREDIMAP_ANONYMOUS, 且 fd = -1;

而在类Unix系统中无此宏定义,可用以下步骤建立(依赖设备目录下的zero文件)

fd = open("/dev/zero”,O_RDWR);
p = mmap(NULL, rsize, PROT_READIPROT_WRITE, MIMAP_SHARED, fd, 0);

 

五、总结

 
 
 
 
  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值