linux就该这么学【进程间通信】

在这里插入图片描述

进程间通信的目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
    在这里插入图片描述

  • 资源共享:多个进程之间共享同样的资源。

  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

进程间通信发展

1、使用文件的方式

管道:特点会比较独立,是使用文件实现的,

2、使用特定的标准

System V进程间通信
POSIX进程间通信

这是两套特定的标准,博主后面会使用System V作为通信的标准,而POSIX会在多线程节将他加入使用起来。站在OS的角度,所谓的标准就是他的接口不能被轻易改变,(其中包括参数个数、接口名、返回值),所以为了保持他们不变我们定义了一些标准,System V、POSIX进程间通信。, 标准的本意就是为了让大家达成共识,降低程序员之间沟通的成本。

管道

什么是管道

  • 管道是Unix中最古老的进程间通信的形式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道“,而它其实是以文件的方式完成的进程通信,

管道的工作原理

管道又分为匿名管道和命名管道,比如以下的使用who | wc -l就是使用匿名管道的方式,who进程会将他本身的数据写入到管道当中,而wc -l进程会去管道中读取,这个过程两个进程是会共享同一块资源的,而完成了进程的通信。

在这里插入图片描述

假设我们新创建了文件是被分配到的fd是3,默认的0、1、2(stdout、stdin、stderr)已经被打开了,他是会被bash打开的,而后继创建的子进程都会继承父进程的file__strucrt、task_struct那么子进程和父进程都能指向同一块内存空间,他们是共享的

在这里插入图片描述

1、父进程创建管道

我们在创建管道的时候父进程为了需要读写,会把管道的读和写两个权限打开,这样父进程就可以做到能去管道读取数据和去管道写入数据。
在这里插入图片描述

2、父进程fork子进程

当父进程创建完子进程后,子进程会以模板的方式拷贝一份父进程的file_strucrt,而file_struct这个结构体中会有fd_arr,所以fd_arr这个数组中的内容是一样的,那么父进程以读写的方式创建了管道,并且指向它,子进程就也会指向这个管道,并且也是以读写的形式。
在这里插入图片描述

3、父进程关闭读权限,子进程关闭写权限

进程间通信是一个单向通信方式,也就是父进程只拥有写入权限,而子进程只拥有读入权限,那么就需要在fork创建子进程后,将父进程的fd_arr[0]关闭, 将子进程的fd_arr[1]关闭,那么父进程就只剩下写入的权限,而父进程还剩下读取的权限。也就完成了管道的单向通信,如果想要完成双向通信那么就需要创建两个管道, 那么两个进程就都拥有了读写权限。
在这里插入图片描述

匿名管道

文档

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

父进程创建管道和子进程,那么父子进程都可以使用管道完成进程通信,那么匿名管道只会让拥有亲缘关系的两个进程完成通信,常用于父子关系

结合代码理解管道的工作原理

在实际写代码的过程中,父进程创建完管道之后再来创建子进程,从而让父子进程都指向同一块内存空间,但是能不能用缓存区让父子进程指向同一块空间完成进程通信呢?
在这里插入图片描述
使用缓冲区,站在进程的角度去思考这个问题时,我们都知道子进程是以父进程为模板创建的,子进程拥有的程序地址空间、页表都是父进程的一份拷贝,而他们也会映射出同一块物理内存,但是父子之间有任何的一方需要对共享的内存进行修改了,那么就会发生写时拷贝,他们会拥有自己独立的数据并各自私有化,那么就不存在完全共享了,而进程通信的目的就是为了让两个进程共享同一份资源,所以使用缓冲区是不可的。
在这里插入图片描述

为了完成匿名管道的单向通信,我们需要只保留父进程的写入权限和子进程的读入权限,所以需要将父进程的读入权限关闭,子进程的写入权限关闭,原理如同下面的关系模型。
在这里插入图片描述

这里我们使用子进程去往管道写入数据,而让父进程从管道中读取数据

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

int main()
{
   
    int pipefd[2] = {0}; //存放文件描述符
    int t = pipe(pipefd); //父进程创建管道, pipe的参数是一个输出型参数
    assert(t == 0);

    pid_t id = fork();
    if(id < 0) perror("fork 失败");
    else if(id > 0){
        close(pipefd[1]); //关闭父进程的写入权限
        //父进程从管道读取数据
        char buff[666];
        while(1){
            ssize_t ret = read(pipefd[0] ,buff,sizeof(buff) - 1);  //子进程以读取权限从管道读入数据
            //ret存放的是实际读取的字节个数
            if(ret > 0){
                buff[ret] = 0;
                printf("父进程读取的数据:%s", buff); 
            }
            sleep(1);
        }
        printf(buff);
          
    }else{ 
        close(pipefd[0]); //关闭子进程的读取权限
        //子进程往管道写入数据
        const char *str = "child write\n";  //存放'\n'的作用是刷新缓冲区
        while(1) {
            write(pipefd[1], str,strlen(str));//以写权限向管道写入数据
            sleep(1);
        }   
    }
    return 0;
}

程序运行结果:
在这里插入图片描述

以上就是使用管道完成父子进程间通信

实验1

如果我们让子进程每隔5秒后向子进程写入数据后会发生什么呢?
在这里插入图片描述
当子进程每隔5秒写入一次child write,那么父进程会以阻塞的方式等待子进程,因为父进程啥事也没干,sleep执行期间子进程并没有往管道中写入数据,父进程其实等待的是管道中的数据,只有管道有数据了,父进程才能读取到,并输出,在这个期间OS会将父进程PCB的R状态设置为S状态,并将父进程由运行时队列放入进等待队列。
在这里插入图片描述
结论1:当我们实际在读入时,如果读入条件不满足(子进程写入慢,管道为空),就需要对读入端进行阻塞,这里阻塞的是父进程

实验2

前面是由于子进程写入数据太慢,让父进程一直等待,我们能不能让子进程写入快一点呢?而让父进程读慢一点
在这里插入图片描述
程序运行结果:
在这里插入图片描述

结论2:当我们实际在写入时,如果写入条件不满足(子进程输入快,父进程读取慢,管道对应的文件缓冲区被占满),就需要对写入端进行阻塞,这里阻塞的是子进程

总结:

在管道当中,不管是读入还是写入,只要读入/写入 条件不满足那么读入方或者写入方都需要被阻塞

补充:

管道他是一个缓冲区,管道是有水位线这样的概念的,当管道的水位线低于低水位线(也就是管道为空的情况),那么在实际读取的时候,就没办法获取数据,那么就需要对读入方进行阻塞直到管道的水位等于或者大于低水位线,那么就可以读取,而写入的原理就是相反了,当管道的水位大于最高水位线(管道满了),那么就不再写入,而写入方就需要被阻塞。

问题:

问题?不管是父进程读入需要被阻塞(管道空)还是子进程写入需要被阻塞(管道满),但是他们之间是怎么知道对方已经需要被阻塞呢?

其实一个进程的影响会导致宁外一个进程受到影响这叫做进程间同步,比如父进程在实际读取的时候,管道为空就需要以阻塞的方式等待子进程将数据写入到管道,管道里有数据了,继续向管道读取数据,从而完成读取数据的操作。

结论3:当写端关闭文件描述符后,读端将管道的数据读取完毕之后, 会读到文件结尾,那么read的返回值就是是0,

结论4:如果读数据方将读权限关闭了pipefd[0],那么就算子进程一直往管道写入数据,也并不会一直运行,因为一旦写满后,子进程再写就会造成系统资源的浪费,而操作系统为了节省资源会把子进程杀掉,注意:子进程被操作系统杀掉后,需要被父进程回收,否则子进程会进入僵尸这里父进程需要以waitpid的方式等待子进程

管道读写规则

当没有数据可读时

  • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
  • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

当管道满的时候

  • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
  • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

其他情况

  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性

管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
    在这里插入图片描述

站在内核角度-管道本质
在这里插入图片描述
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”

命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件

实验,我们想用一个没有任何关系的进程完成进程间通信,可以使用命名管道,

创建一个管道

命名管道可以从命令行上创建,命令行方法是使用下面这个命令
mkfifo filename

mkfifo函数的作用是在文件系统中创建一个文件,该文件用于提供FIFO功能,即命名管道。前边讲的那些管道都没有名字,因此它们被称为匿名管道,或简称管道。对文件系统来说,匿名管道是不可见的,它的作用仅限于在父进程和子进程两个进程间进行 通信。而命名管道是一个可见的文件,因此,它可以用于任何两个进程之间的通信,不管这两个进程是不是父子进程,也不管这两个进程之间有没有关系。Mkfifo函数的原型如下所示:

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

mkfifo函数需要两个参数,第一个参数(pathname)是将要在文件系统中创建的一个专用文件。第二个参数(mode)用来规定FIFO的读写权限Mkfifo函数如果调用成功的话,返回值为0;如果调用失败返回值为-1。下面我们以一个实例来说明如何使用mkfifo函数建一个fifo,具体代码如下所示:

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

int main()
{ 
    umask(0); //设置umask的值不再使用系统默认的umask值
    //创建管道
    int ret = mkfifo("./fifo", 0666);
    assert(ret >= 0);
    
    return 0;
}

执行a.out后会为我们生成一个可执行程序,注意:凡是管道的文件,类型都是以p开头的,文件的类型是由开头的第一个字母决定的,并且我们可以看到整个命名管道的文件大小是0值
在这里插入图片描述

实验:借助命名管道完成两个进程之间的通信,使用命名管道完成进程的通信是需要让两个进程共享同一个管道文件的资源
创建客户端进程和服务器进程
编写makefile
在这里插入图片描述

severse.c文件

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

#define FIFO_PATH "./fifo"
int main()
{
    umask(0);
    //severse创建管道
    //往管道写入数据
    int ret = mkfifo("./fifo", 0666);  //创建管道
    int fd = open(FIFO_PATH, O_RDONLY);  //打开管道文件                                                  
    if(fd > 0) {
      char buf[64];
      while(1){
        int ret = read(fd, buf, sizeof(buf) - 1);
        //将管道文件的中的数据读入到buf数组
        buf[ret] = 0;
        
        if(ret > 0){
            printf(buf);
        }
        else if(ret == 0){  //如果客户端已经不再写入数据了,那么severse端就需要被关闭
            
            printf("client quit");
            break;
        }
        else{
            perror("read filed\n");
            break;
        }
      } 
    }
  
 
   return 0;
}

client.c文件

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

#define FIFO_PATH "./fifo"
int main()
{
    //打开管道文件
    int fd = open(FIFO_PATH, O_WRONLY);
   
    char buf[64] = {0};
    //从标准输入当中读取数据存放进buf
   
    while(1)
    {
        printf("请输入信息:");
        //fflush(stdout);
        //从标准输入获取数据写入进管道。
        ssize_t ret = read(0, buf, sizeof(buf) - 1);
        if(ret > 0){ //将读取到的数据往文件中写入
          buf[ret] = 0;
          write(fd, buf, sizeof(buf) - 1);
        }
        else {
          perror("read");
        }

    }
   

    return 0;
}
make 
./server
./client

程序运行结果:
在这里插入图片描述
解释上述行为,当我们将两个进程执行起来后,可以看到窗口中他们之间是能相互通信的,clien进程可以将数据写入到管道文件fifo中,severse进程可以从管道中读取数据再向屏幕上输出,通信环境是在内存中完成通信的,数据并不会刷新到硬盘上。

system v共享内存

页表中保存的每一项是一组虚拟地址和物理地址,假设前提是32平台,而每一个物理地址他是占用4个字节的,虚拟地址同样是占用4 字节,页表最大能存放2^32 项,那么 2^32 * 8肯定会超出我们的内存资源,所以在进程的页表中是不可能会存放下这么多的地址的,那么他究竟是以什么技术支持的呢?

我们实际查找虚拟地址的时候可以将一个页表分为 10 、 10 、 12,这样的组合就是32位了, 页表也是有分级管理的,可以用第一项去维护2^10个页表项 第二项去维护2^10 个页表项 ,第三项去维护2^12 个页表项
在这里插入图片描述

站在进程的角度理解什么是共享内存

OS的地址空间是借助进程的地址空间来完成的,而进程可以拥有多个但是内核区只有一个,
页表可以映射出虚拟地址和物理地址的关联关系,可以产生多个(进程的独立性),他是用户级别的,而我们的内核也需要通过页表建立映射关系从而访问操作系统的代码,而内核的页表是属于内核级别的页表

在这里插入图片描述
而如果想要完成两个独立的进程之间完成进程通信就需要让这两个进程共享着同一份资源。
而在进程地址空间上会申请出一份内存这个共享区,共享区存放一个虚拟地址,通过页表的映射找到对应的物理内存,宁外一个进程也是如此,从而两个进程就可以通过共享区找到对应的物理内存了。
在这里插入图片描述
问题?两个进程都能看到物理内存上的共享数据吗?

可以,因为页表中已经存在了物理地址和虚拟地址的映射关系,而进程地址空间的共享区可以通过页表的映射关系找到对应的物理内存,所以可以直接访问,

共享内存示意图:

在这里插入图片描述

system V共享内存的好处

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据

共享内存需要的4个基本操作

1、创建共享内存
2、删除共享内存
3、关联共享内存
4、取消关联共享内存

共享内存需要的四个操作的原因:
1、进程间如果需要通信就需要向操作系统申请资源,而操作系统为了让进程之间共享的是同一块资源急需要对进行进程通信的资源管理起来,也是需要先描述后组织的,而OS需要的考虑的是什么时候申请内存、如果分配、当前的共享内存挂接了多少个进程、记录和哪个进程相关联,和什么进程不关联,而定义这些行为需要使用数据结构管理起来,所以共享内存也需要先描述后组织

管理共享内存的数据结构是以下定义。

在Linux内核中,每个共享内存都由一个名为 struct shmid_kernel的结构体来管理,而且Linux限制了系统最大能创建的共享内存为128个。通过类型为 struct shmid_kernel结构的数组来管理,如下:
在这里插入图片描述
系统为每一个IPC对象保存一个ipc_perm结构体,该结构说明了IPC对象的权限和所有者,每一个版本的内核各有不用的ipc_perm结构成员。key值的作用是标识一块ipc资源,可以让多个进程关联到同一块内存资源
在这里插入图片描述在这里插入图片描述
shm_npages 字段表示共享内存使用了多少个内存页。而 shm_pages 字段指向了共享内存映射的虚拟内存页表项数组等。另外 struct shmid_ds 结构体用于管理共享内存的信息,而 shm_segs数组 用于管理系统中所有的共享内存。

管理共享内存的系统调用

ftok函数

#include<sys/ipc.h>
#include<sys/types.h>
key_t ftok(const char *pathname, int proj_id);
#pathname: 指定的文件名,该文件必须是存在而且可以访问
#proj_id:子序号,只有8个比特被使用(0-255)范围值
#当成功执行时,返回一个key_t值,失败返回-1

ftok实现原理

ftok返回的key_t在Linux中是一个32位的值,它通过取proj_id参数的最低8个有效位、包含pathname指定文件所属的文件系统的设备的次要设备号的最低8个有效位以及pathname所指定文件的i-node号的最低16个有效位组成。

注意:ftok只是在用户层进行申请,并不会涉及系统内核

shmget函数

#include<sys/ipc.h>
#include<sys/shm.h>
shmget
int shmget(key_t  key, size_t  size, int  flag);
key: 标识符的规则
size:共享存储段的字节数
flag:读写的权限,由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的

IPC_CREAT | IPC_EXCL 这两个权限组合表示的含义是:
如果共享内存没有创建那么就创建并返回,否则已经存在就直接出错。 

返回值:成功返回共享存储的id,失败返回-1

shmget函数会创建一个共享内存,并将key值填入进程ipc_perm对象中的成员变量key上,相当于人为的给这快ipc_perm资源赋予了一个key值

shmget函数使用IPC_CREAT | IPC_EXCL权限创建共享内存细节问题,对于已经存在共享内存,再次创建就会出错,解释: 共享内存的创建之后的资源不随进程直接结束,从而结束,ipc资源的生命周期而是伴随着系统内核的,需要被操作系统释放,(他不同于管道,管道是通过文件的方式创建资源,属于文件系统的,而文件会随着进程的结束而结束)后面博主会讲申请出来的ipc资源该如何释放
在这里插入图片描述
使用 ipcs -m命令可以直接查看创建的共享内存资源, nattch表示当前共享内存的挂接数目前是0个进程挂接了共享内存。
在这里插入图片描述
共享内存的大小挂接数可以在shmid_ds这个结构体变量中可以查看
在这里插入图片描述
使用该命令ipcrm -m xxx(shmid编号)可以删除刚刚创建的共享内存
在这里插入图片描述
注意:共享内存的生命周期是随着系统内核的,所以我们在创建的时候必须要将共享内存给释放完毕,也可以使用下面的系统调用接口删除共享内存。
shmctl函数

功能用于控制共享内存
原型int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:由shmget返回的共享内存标识码,它可以定位共享内存
cmd:将要采取的动作(有三个可取值)
buf指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

cmd参数的三个取值:

命令说明
IPC_STAT把shmid_ds结构中的数据设置为共享内存的当前关联值
IPC_SET在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值
IPC_RMID删除共享内存段
//使用系统调用接口删除创建的共享内存
shmctl(ret, IPC_RMID, NULL);  

ret是ipc的shmid
IPC_RMID选项表示的是删除共享内存段
第三个参数是一个输出型参数,如果我们不需要获取共享内存的模式状态和访问权限可以传入NULL

程序执行7秒后将创建出来的共享内存段直接释放
在这里插入图片描述
man手册更新: yum install man-pages

shmat函数

#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);

相关说明:
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
功能:将共享内存段与当前进程脱离
原型void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:共享内存标识id
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回 -1

与shmat相反的一个函数是shmdt,它可以取消当前进程和共享内存的链接状态让当前进程脱离

shmdt函数

功能:将共享内存段与当前进程脱离
原型int shmdt(const void *shmaddr);
shmaddr:由shmat所返回的指针
返回值:成功返回0;失败返回-1

注意:将共享内存段与当前进程脱离不等于删除共享内存段,只是取消了共享内存的引用计数

#include <stdio.h>
#include <sys/stat.h> 
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "comm.h"

int main()
{
    //分配key
    key_t key = ftok(PATHNAME, PROJ_ID);
    printf("key val is %p\n", key);
    //创建共享内存
    int ret = shmget(key, SIZE, IPC_CREAT | 
                     IPC_EXCL | 0666);
    printf("shmid : %d\n", ret);
    if(ret < 0){
        perror("shmet filed\n");
        return 1;
    }
    sleep(7);  //创建共享内存后等待七秒
    
    //为共享内存创建连接数
    char * str = shmat(ret, NULL, 0);  //1、参数为内存表示符
    sleep(7);//创建完连接数后等待七秒

    //取消共享内存的关联
    shmdt(str);
    sleep(7);//取消当前进程和共享内存连接数后等待七秒
    
    //使用系统调用接口删除创建的共享内存
    shmctl(ret, IPC_RMID, NULL);
    sleep(7);
  
    

    return 0;
}

以下是程序运行结果,读者可以添加下面这段运行脚本观察现象
while :; do ipcs -m; sleep 1; echo "###########"; done
在这里插入图片描述

使用共享内存完成进程间通信

client.c 文件

#include <stdio.h>
#include <sys/stat.h> 
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "comm.h"

int main()
{
    //分配key
    key_t key = ftok(PATHNAME, PROJ_ID);
    printf("key val is %p\n", key);
    //创建共享内存
    int ret = shmget(key, SIZE, IPC_CREAT | 
                     IPC_EXCL | 0666);
    printf("shmid : %d\n", ret);
    if(ret < 0){
        perror("shmet filed\n");
        return 1;
    }


    //为共享内存创建连接数
    char * str = shmat(ret, NULL, 0);  //1、参数为内存表示符
    sleep(7);
    //向共享内存读入数据
    int count = 16;
    while(count--){
        printf("%s\n", str);
        fflush(stdout);
        sleep(1);
    }
    //取消共享内存的关联
    shmdt(str);
    sleep(7);
    //使用系统调用接口删除创建的共享内存
    shmctl(ret, IPC_RMID, NULL);
    sleep(7);
  
    

    return 0;
}

server.c文件

#include <stdio.h>
#include <sys/stat.h> 
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "comm.h"

int main()
{
    //分配key
    key_t key = ftok(PATHNAME, PROJ_ID);
    printf("key val is %p\n", key);
    //打开共享内存
    int ret = shmget(key, SIZE, 0);
    printf("shmid : %d\n", ret);
    if(ret < 0){
        perror("shmet filed\n");
        return 1;
    }

    //为共享内存创建连接数
    char * str = shmat(ret, NULL, 0);  //1、参数为内存表示符
    sleep(7);

    //客户端不断向共享内存中写入数据。
    char ch = 'a';
    for(; ch <= 'z'; ch++){
        str[ch - 'a'] = ch;
        sleep(5);      
    }

    //取消共享内存的关联
    shmdt(str);
  
    

    return 0;
}

makefile文件

.PHONY:all
all:server client

server:server.c
	gcc server.c -o server 
client:client.c
	gcc client.c -o client 

.PHONY:clean
clean:
	rm -rf server client 

程序运行结果如下:
在这里插入图片描述
通过实验现象发现即使共享内存中没有数据,client端并没有写入数据,但是sever端却也不会以阻塞的方式等待client(通过一开始输出空格就能发现),结论:共享内存底层不支持任何同步与互斥机制!

了解

消息队列

  • 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
  • 特性方面:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

system V信号量

信号量主要用于同步和互斥的,下面先来看看什么是同步和互斥

需要互斥的前提条件是:
多个进程共享同一份资源,这样就可以使用共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,从而极大的提高了效率,但是进而引发的问题是进程间数据不一致的问题(谁先读,谁先写,如果数据写入出错怎么办的问题),而解决他的方法比较常见的做法就是互斥,任何一方操作临界资源的时候,另外一方不能操作临界资源,

1、临界资源:多个进程在访问同一份资源的时候,这个资源就是临界资源,比如共享内存和管道。
2、通常我们把访问临界资源的代码叫做临界区。
3、为了保护临界资源我们需要对临界区进行加锁的操作,而锁就是信号量。

进程互斥

  • 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥
  • 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
  • 在进程中涉及到互斥资源的程序段叫临界区
  • 特性方面:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。

在这里插入图片描述

  • 20
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
进程间通信是指在不同进程之间进行数据交换和同步的一种技术。Linux提供了多种进程间通信的方式,包括管道、消息队列、共享内存和信号量等。在实验六中,我们将习如何使用这些方式进行进程间通信。 1. 管道 管道是一种半双工的通信方式,它可以在两个进程之间传递数据。在Linux中,管道分为匿名管道和命名管道。匿名管道只能用于父子进程之间的通信,而命名管道可以用于任意两个进程之间的通信。 使用匿名管道进行进程间通信的步骤如下: - 父进程创建管道,并调用fork函数创建子进程。 - 子进程通过管道接收数据。 - 父进程通过管道数据。 - 子进程接收到数据后进行处理。 使用命名管道进行进程间通信的步骤如下: - 创建命名管道。 - 打开命名管道并进行读写操作。 2. 消息队列 消息队列是一种进程间通信机制,它允许不同进程之间通过一个消息传递序列来进行通信。在Linux中,每个消息都有一个类型,接收进程可以选择接收某个特定类型的消息。 使用消息队列进行进程间通信的步骤如下: - 创建消息队列。 - 送消息到消息队列。 - 接收消息并进行处理。 3. 共享内存 共享内存是一种进程间通信的方式,它允许不同进程之间共享同一个物理内存区域。这种方式比较高效,但需要考虑进程间的同步和互斥问题,否则会出现数据不一致的情况。 使用共享内存进行进程间通信的步骤如下: - 创建共享内存区域。 - 进程通过共享内存区域进行数据交换。 - 进程需要进行同步和互斥操作。 4. 信号量 信号量是一种进程间同步的机制,它可以用来保证不同进程之间的共享资源在同一时刻只能被一个进程访问。在Linux中,每个信号量都有一个计数器,当计数器为0时,进程需要等待;当计数器大于0时,进程可以继续执行。 使用信号量进行进程间通信的步骤如下: - 创建信号量。 - 进程对信号量进行P操作(等待)。 - 进程对信号量进行V操作(释放)。 总体来说,不同的进程间通信方式各有优缺点,应根据实际需求选择适合的方式。在实验六中,我们将通过编写代码来习如何使用这些方式进行进程间通信

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱生活,爱代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值