Linux进程间通信

1. 进程间通信介绍

1-1进程间通信⽬的

• 数据传输:⼀个进程需要将它的数据发送给另⼀个进程

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

• 通知事件:⼀个进程需要向另⼀个或⼀组进程发送消息,通知它(它们)发⽣了某种事件(如进程终⽌时要通知⽗进程)。

• 进程控制:有些进程希望完全控制另⼀个进程的执⾏(如Debug进程),此时控制进程希望能够

拦截另⼀个进程的所有陷⼊和异常,并能够及时知道它的状态改变。

进程通信的前提本质是:可以让不同的进程看到同一份资源

1-2进程间通信发展

• 管道

• SystemV进程间通信

• POSIX进程间通信
1-3进程间通信分类

管道

• 匿名管道pipe 基于文件

• 命名管道 单独设计通信板块

SystemVIPC 网络

• SystemV消息队列

• SystemV共享内存

• SystemV信号量

POSIXIPC

• 消息队列

• 共享内存

• 信号量

• 互斥量

• 条件变量

• 读写锁

2.管道

bash 复制代码
lzy@VM-0-14-ubuntu:/root$ who | wc -l
1

其中,who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据打到"管道"当中,wc进程再通过标准输入从"管道"当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。

2.1什么是管道?

父子进程向同一个标准输出打印,本质上是共用同一个标准输出文件。

为什么 "文件缓冲区" 不进行写时拷贝?

"文件缓冲区" 通常指的是内核的 页缓存(Page Cache)。

写时拷贝(COW)的作用域

COW 主要是针对 用户空间的虚拟内存 设计的。

fork() 发生时,子进程的虚拟内存页表指向父进程的物理页。

只有当父子进程中某一方试图修改内存时,内核才会真正复制一份物理页面。

页缓存(Page Cache)的特殊性

页缓存属于内核空间,是全局共享的资源,它不归属于某个特定的进程。

目的是为了效率:如果每个进程都把文件在内存里复制一份,既浪费内存,又丧失了通过缓存加速读取的意义。

由 inode 关联:如前所述,inode 通过 address_space 管理页缓存。无论有多少个进程(父子或无关)打开这个文件,它们最终都通过 inode 找到同一份页缓存。

总结

父子进程共享的不仅是缓冲区,还有文件状态。

因为它们共享 struct file,所以连文件读写指针(f_pos)都是共享的。

例子:如果父进程 read 了 10 个字节,子进程再去 read,会从第 11 个字节开始读,而不是从头开始。

COW 不发生在内核缓冲区。

对文件的写入直接反映在由内核管理的、全局唯一的页缓存中(随后由内核负责刷回磁盘)。

所以父子进程通过共享内核中的 struct file 对象,进而操作同一个页缓存(文件缓冲区),由于该缓存属于内核全局资源而非进程私有内存,因此不适用也不需要写时拷贝机制。

2.1匿名管道

pipe函数

pipe函数用于创建匿名管道,pip函数的函数原型如下:
int pipe(int pipefd[2]);

pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:

pipefd[0] 管道的读端描述符

pipefd[1] 管道的写端描述符

pipe函数调用成功时返回0,调用失败时返回-1。

匿名管道使用步骤

在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:

1.父进程调pipe()创建管道

2.父进程创建子进程

3.父进程关闭写端,子进程关闭读端

注意:

管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。

从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取。

站在文件描述符的角度再来看看这三个步骤:

代码

cpp 复制代码
#include<stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include<stdlib.h>
#include<string.h>

int main()
{
   //1.创建管道
   int pipefd[2];
   int n = pipe(pipefd);
   if(n<0)
   {
      perror("pipe");
      return 1;
   }
   printf("pipefd[0]:%d  pipefd[1]:%d\n",pipefd[0],pipefd[1]);

   //2.创建子进程
   pid_t id = fork();
   if(id==0)
   {
      //子进程写
      close(pipefd[0]);
      char *msg = "hello,linux";
      write(pipefd[1],msg,strlen(msg));

      exit(0);
   }
   //父进程读
   close(pipefd[1]);
   char buff[64];
  ssize_t bytes_read = read(pipefd[0], buff, sizeof(buff) - 1); // 减1留位置给\0
if (bytes_read == -1) {
    // 2. 处理错误
    perror("read failed");
} else if (bytes_read == 0) {
    // 3. 处理对端关闭(EOF)
    printf("Pipe closed by writer.\n");
} else {
    // 4. 手动加字符串结束符(关键)
    buff[bytes_read] = '\0'; 
    printf("Received %zd bytes: %s\n", bytes_read, buff);
}

pid_t rid = waitpid(id,NULL,0);

   return 0;
}
bash 复制代码
lzy@VM-0-14-ubuntu:~/hello$ ./test
pipefd[0]:3  pipefd[1]:4
Received 11 bytes: hello,linux

管道的四种情况和五大特性
匿名管道特性

1.常用于有血缘关系的进程,进行IPC,常用于父子。
2.单向通道
3.管道的生命周期随进程
4.面向字节流
5.管道自带同步互斥机制

我们将一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。

一、先明确两个概念

互斥:同一时刻,只能有一个进程写管道,或一个进程读管道,不会出现数据混乱。

同步:读进程会等待写进程写入数据;写进程会等待读进程读取数据

管道的原子性只限制在一次 write 内:

如果写入数据 ≤ PIPE_BUF(Linux 通常是 4096 字节):绝对原子、绝对互斥。

如果写入数据 > PIPE_BUF:内核不保证原子性,可能被拆分,此时需要自己加锁。

日常使用中,只要不超大块写入,完全依赖管道自带同步互斥就足够安全。

管道的四种情况

1.管道里没有数据,读端会被阻塞(写端不写,写端不关)

对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。

2.管道里写满就不写了(读端不读,读端不关)

对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。

3.写端不写,写端关闭,read会读到返回值为0,表示读到文件结束

4.读关闭,写正常。OS会自动杀掉进程

前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。

我们可以通过以下代码看看情况四中,子进程退出时究竟是收到了什么信号。

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    int pipefd[2] = {0};
    //创建管道
    int n = pipe(pipefd);
    if(n<0)
    {
        perror("pipe()");
        return 1;
    }
    //创建子进程
    pid_t id = fork();
    if(id==0)
    {
        //子进程关闭读端
        close(pipefd[0]);
        char *msg = "hello,im zi jin cheng";
        write(pipefd[1],msg,strlen(msg));
        //写完关闭文件
        close(pipefd[1]);
        exit(0);
    }
    else
    {
        //父进程关闭写端
        close(pipefd[1]);
        //主动关闭读端
        close(pipefd[0]);
        int status = 0;
        waitpid(id,&status,0);
        //打印子进程收到的信号
        printf("child get signal %d\n",status & 0x7F);
        return 0;
 
    }
    
}

运行结果显示,子进程退出时收到的是13号信号。

bash 复制代码
lzy@VM-0-14-ubuntu:~/hello$ ./test.03.10
child get signal 13

3.1命名管道

A、B 进程通过同一个 inode 找到内核中唯一的管道对象,因此共享同一缓冲区,从而实现进程间通信。

命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名 打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。

3.2创建一个命名管道

在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:
int mkfifo(const char *pathname, mode_t mode);

1.mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。

若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。

若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。

2.mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。

例如,将mode设置为0666,则命名管道文件创建出来的权限如下:

rw-rw-rw-

但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。

rw-rw-r--

若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。

mkfifo函数的返回值。

命名管道创建成功,返回0。

命名管道创建失败,返回-1。

命名管道的打开规则

• 如果当前打开操作是为读⽽打开FIFO时

◦ O_NONBLOCK=disable:阻塞直到有相应进程为写⽽打开该FIFO

◦ O_NONBLOCK=enable:⽴刻返回成功

• 如果当前打开操作是为写⽽打开FIFO时

◦ O_NONBLOCK=disable:阻塞直到有相应进程为读⽽打开该FIFO

◦ O_NONBLOCK=enable:⽴刻返回失败,错误码为ENXIO

用命名管道实现serve&client通信

实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。

服务端的代码如下:

cpp 复制代码
#include"comm.h"

int main()
{
    umask(0);
    //创建管道
    int n = mkfifo(FILE_NAME,0666);
    if(n<0)
    {
        perror("mkfifo");
        return 1;
    }

    int fd = open(FILE_NAME,O_RDONLY);//以读方式打开管道
    if(fd<0)
    {
        perror("open");
        return 2;
    }
    char msg[128];
    while(1)
    {
        msg[0] = '\0';
        //从管道中读取信息
        ssize_t s = read(fd,msg,sizeof(msg)-1);
        if(s>0)
        {
            msg[s] = '\0';
            printf("client# %s\n",msg);//输出客户端发来的消息
        }
        else if(s==0)
        {
            printf("client quit\n");
            break;
        }
        else
        {
            printf("read error\n");
            break;
        }
    }
    //关闭管道
    close(fd);
    return 0;
}

而对于客户端来说,因为服务端运行起来后命名管道文件就已经被创建了,所以客户端只需以写的方式打开该命名管道文件,之后客户端就可以将通信信息写入到命名管道文件当中,进而实现和服务端的通信。

客户端的代码如下:

cpp 复制代码
#include"comm.h"

int main()
{
    int fd = open(FILE_NAME,O_WRONLY);//以写的方式打开管道
    if(fd<0)
    {
        perror("open");
        return 1;

    }
    char msg[128];
    while(1)
    {
        msg[0] = '\0';
        printf("Please Enter# ");//提示客户端输入
        fflush(stdout);
        //从客户端的标准输入读取数据
        ssize_t s = read(0,msg,sizeof(msg)-1);
        if(s>0)
        {
            msg[s-1] = '\0';
            //将信息写到客户端管道中
            write(fd,msg,strlen(msg));

        }
    }
    close(fd);
    return 0;
}

头文件

cpp 复制代码
#pragma once
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>

#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道

代码编写完毕后,先将服务端进程运行起来,接着再将客户端也运行起来,此时我们从客户端写入的信息被客户端写入到命名管道当中,服务端再从命名管道当中将信息读取出来打印在服务端的显示器上,该现象说明服务端是能够通过命名管道获取到客户端发来的信息的,换句话说,此时这两个进程之间是能够通信的。

当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了)。

双方进程之间的通信是在内存当中进行的,和匿名管道通信是一样的。与硬盘无关

命名管道和匿名管道的区别

匿名管道由pipe函数创建并打开。

命名管道由mkfifo函数创建,由open函数打开。

FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。

4.systemV共享内存

管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份由操作系统提供的资源。

system V IPC提供的通信方式有以下三种:

system V共享内存

system V消息队列

system V信号量

4.1system V共享内存

共享内存的基本原理

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

1.开辟内存块

2.映射到对应的进程的虚拟地址空间

3.删除共享内存

注意:

这里所说的开辟物理空间、建立映射等操作都是调用系统接口完成的,也就是说这些动作都由操作系统来完成。

共享内存数据结构

在操作系统中一定有大量的共享内存,所以操作系统要对其进行管理(先描述,在组织)

共享内存的数据结构如下:

cpp 复制代码
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment
(bytes) */
__kernel_time_t shm_atime; /* last attach time
*/
__kernel_time_t shm_dtime; /* last detach time
*/
__kernel_time_t shm_ctime; /* last change time
*/
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current
attaches */
unsigned short shm_unused; /* compatibility */
void shm_unused2; / ditto - used by
DIPC */
void shm_unused3; / unused */
};

当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性。

可以看到上面共享内存数据结构的第一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在shm_perm这个结构体变量当中,其中ipc_perm结构体的定义如下:

cpp 复制代码
struct ipc_perm{
	__kernel_key_t  key;
	__kernel_uid_t  uid;
	__kernel_gid_t  gid;
	__kernel_uid_t  cuid;
	__kernel_gid_t  cgid;
	__kernel_mode_t mode;
	unsigned short  seq;
};

注意key值全部可见,用来找到共享内存,而shm_id是每个进程操作共享内存的数据

进程 A:用 ftok() 生成 key → 用 shmget(key) 拿到 shm_id → 操作共享内存

进程 B:用相同方式生成同一个 key → 用 shmget(key) 拿到自己的 shm_id → 访问同一块内存

共享内存的创建

创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:

功能:

⽤来创建共享内存

原型

bash 复制代码
int shmget(key_t key, size_t size, int shmflg);

参数

key:这个共享内存段名字

size:共享内存⼤⼩

建议是4096字节的整数倍

shmflg:由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的

取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。

取值为IPC_CREAT | IPC_EXCL:共享内存不存在,创建并返回;共享内存已存在,

出错返回。

返回值:

成功返回⼀个⾮负整数,即该共享内存段的标识码;失败返回-1

传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取

ftok函数的函数原型如下:

bash 复制代码
key_t ftok(const char *pathname, int proj_id);

ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。需要注意的是,pathname所指定的文件必须存在且可存取。

注意:

使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改。

需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源。

代码

cpp 复制代码
#include <stdio.h>
#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <unistd.h>

#define PATHNAME "/home/lzy/test0312/server.c"
#define PROJ_ID 0x667
#define SIZE 4096//共享内存大小

int main()
{
    key_t key = ftok(PATHNAME,PROJ_ID);//获取key值
    if(key<0)
    {
        perror("ftok");
        return 1;
    }
    int shm = shmget(key,SIZE,IPC_CREAT | IPC_EXCL);//创建新的共享内存
    if(shm<0)
    {
        perror("shmget");
        return 2;
    }
    printf("key: %x\n",key);
    printf("shm: %d\n",shm);//打印句柄
    return 0;
}

该代码编写完毕运行后,我们可以看到输出的key值和句柄值:

bash 复制代码
lzy@VM-0-14-ubuntu:~/test0312$ ./server
key: 670209e3
shm: 1

Linux当中,我们可以使用ipcs命令查看有关进程间通信设施的信息。

单独使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:

-q:列出消息队列相关信息。

-m:列出共享内存相关信息。

-s:列出信号量相关信息。

例如,携带-m选项查看共享内存相关信息:

1.key

就是你代码里 ftok 生成的 key!

全系统可见,用来找到共享内存。

2.shmid

就是你代码里 shmget 返回的 shm_id!

操作共享内存的句柄,进程私有。

删除共享内存时用的就是这个 ID。

3.owner

创建这个共享内存的用户名。

4.perms

权限(和文件权限一样)。

5.bytes

共享内存大小。

你代码里定义的 SIZE 4096。

6.nattch

目前有多少个进程挂接在这块内存上。

共享内存的释放

通过上面创建共享内存的实验可以发现,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。

这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。

此时我们若是要将创建的共享内存释放,有两个方法,一就是使用命令释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放。

一使用命令释放共享内存

cpp 复制代码
[cl@VM-0-15-centos shm]$ ipcrm -m shmid

控制共享内存我们需要用shmctl函数,shmctl函数的函数原型如下:

功能:⽤于控制共享内存

原型

bash 复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数

shmid:由shmget返回的共享内存标识码

cmd:将要采取的动作(有三个可取值)

1.IPC_STAT 获取共享内存的当前关联值,此时参数buf作为输出型参数

2.IPC_SET 在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值

3.IPC_RMID 删除共享内存段

buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构

返回值:成功返回0;失败返回-1

例如,在以下代码当中,共享内存被创建,两秒后程序自动移除共享内存,再过两秒程序就会自动退出。

cpp 复制代码
#include <stdio.h>
#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <unistd.h>

#define PATHNAME "/home/lzy/test0312/server.c"
#define PROJ_ID 0x667
#define SIZE 4096//共享内存大小

int main()
{
    key_t key = ftok(PATHNAME,PROJ_ID);//获取key值
    if(key<0)
    {
        perror("ftok");
        return 1;
    }
    int shm = shmget(key,SIZE,IPC_CREAT | IPC_EXCL);//创建新的共享内存
    if(shm<0)
    {
        perror("shmget");
        return 2;
    }
    printf("key: %x\n",key);
    printf("shm: %d\n",shm);//打印句柄

    sleep(2);
    shmctl(shm,IPC_RMID,NULL);//删除共享内存
    sleep(2);
    return 0;
}

共享内存的关联

将共享内存连接到进程地址空间我们需要用shmat函数,shmat函数的函数原型如下:

功能:将共享内存段连接到进程地址空间

原型

bash 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);

参数

shmid: 共享内存标识

shmaddr:指定连接的地址

1.shmaddr为NULL,核⼼⾃动选择⼀个地址

2.shmaddr不为NULL且shmflg⽆SHM_RND标记,则以shmaddr为连接地址。

3.shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会⾃动向下调整为SHMLBA的整数

倍。公式:shmaddr - (shmaddr % SHMLBA)

4.shmflg=SHM_RDONLY,表⽰连接操作⽤来只读共享内存

shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY

1.SHM_RDONLY 关联共享内存后只进行读取操作

2.SHM_RND 若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA)

3.0 默认为读写权限

返回值:成功返回⼀个指针,指向共享内存第⼀个节;失败返回-1

这时我们可以尝试使用shmat函数对共享内存进行关联。

cpp 复制代码
#include <stdio.h>
#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <unistd.h>

#define PATHNAME "/home/lzy/test0312/server.c"
#define PROJ_ID 0x555
#define SIZE 4096//共享内存大小

int main()
{
    key_t key = ftok(PATHNAME,PROJ_ID);//获取key值
    if(key<0)
    {
        perror("ftok");
        return 1;
    }
    int shm = shmget(key,SIZE,IPC_CREAT | IPC_EXCL);//创建新的共享内存
    if(shm<0)
    {
        perror("shmget");
        return 2;
    }
    printf("key: %x\n",key);
    printf("shm: %d\n",shm);//打印句柄

    printf("attach begin!\n");
    char* men = shmat(shm,NULL,0);//关联共享内存
    if(men ==(void*)-1)//-1 的二进制是 全 1
    {
        perror("shmat");
        return 1;
    }
    printf("attach end!\n");

    sleep(2);
    shmctl(shm,IPC_RMID,NULL);//删除共享内存
    sleep(2);
    return 0;
}

代码运行后发现关联失败,主要原因是我们使用shmget函数创建共享内存时,并没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即什么权限都没有,因此server进程没有权限关联该共享内存。

我们应该在使用shmget函数创建共享内存时,在其第三个参数处设置共享内存创建后的权限,权限的设置规则与设置文件权限的规则相同。

bash 复制代码
 int shm = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);//创建新的共享内存

此时再运行程序,即可发现关联该共享内存的进程数由0变成了1,而共享内存的权限显示也不再是0,而是我们设置的666权限。

共享内存的去关联

取消共享内存与进程地址空间之间的关联我们需要用shmdt函数,shmdt函数的函数原型如下:

功能:将共享内存段与当前进程脱离

原型

bash 复制代码
int shmdt(const void *shmaddr);

参数

shmaddr: 由shmat所返回的指针

返回值:成功返回0;失败返回-1

注意:将共享内存段与当前进程脱离不等于删除共享内存段

用共享内存实现serve&client通信

代码

server.c

cpp 复制代码
#include"comm.h"

int main()
{
    key_t key  = ftok(PATHNAME,PROJ_ID);//获取键值
    if(key<0)
    {
        perror("ftok");
        return 1;
    }

    int shm = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);//创建新的共享内存
    if(shm<0)
    {
        perror("shmget");
        return 2;
    }

    printf("key : %x\n",key);
    printf("shm : %d\n",shm);

    char* men = shmat(shm,NULL,0);//关联共享内存
    while(1)
    {
        //不进行操作
    }

    shmdt(men);//共享内存去关联
    shmctl(shm,IPC_RMID,NULL);//释放共享内存
    return 0;
}

client.c

cpp 复制代码
#include"comm.h"

int main()
{
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key<0)
    {
        perror("ftok");
        return 1;
    }

    int shm = shmget(key,SIZE,IPC_CREAT);//获取共享内存ID
    if(shm<0)
    {
        perror("shmget");
        return 2;
    }

    printf("key : %x\n",key);
    printf("shm : %d\n",shm);

    char* men = shmat(shm,NULL,0);//关联共享内存

    int i = 0;
    while(1)
    {
        //
    }

    shmdt(men);//去关联
    return 0;
}

comm.h

cpp 复制代码
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define PATHNAME "/home/lzy/test0314"
#define PROJ_ID 0x555
#define SIZE 4096

先后运行服务端和客户端后,通过监控脚本可以看到服务端和客户端所关联的是同一个共享内存,共享内存关联的进程数也是2,表示服务端和客户端挂接共享内存成功。

此时我们就可以让服务端和客户端进行通信了,这里以简单的发送字符串为例。

cpp 复制代码
int i = 0;
    while(1)
    {
      //客户端不断向共享内存写入数据 
	  men[i] = 'A' + i;
	  i++;
	  men[i] = '\0';
	  sleep(1);
    }

服务端不断读取共享内存当中的数据并输出:

cpp 复制代码
//服务端不断读取共享内存当中的数据并输出
    while (1)
    {
	  printf("client# %s\n", men);
	  sleep(1);
    }

此时先运行服务端创建共享内存,当我们运行客户端时服务端就开始不断输出数据,说明服务端和客户端是能够正常通信的。

共享内存与管道进行对比

当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式。

从这张图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:

1.服务端将信息从输入文件复制到服务端的临时缓冲区中。

2.将服务端临时缓冲区的信息复制到管道中。

3.客户端将信息从管道复制到客户端的缓冲区中。

4.将客户端临时缓冲区的信息复制到输出文件中。

从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:

从输入文件到共享内存。

从共享内存到输出文件。

所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少。

但是共享内存也是有缺点的,我们知道管道是自带同步与互斥机制的,但是共享内存并没有提供任何的保护机制,包括同步与互斥。

共享内存访问方式

共享内存通过 shmat() 挂接函数的返回值( void* 指针)直接访问,无需额外系统调用。

该指针指向共享内存在当前进程虚拟地址空间的起始位置,可像普通内存一样读写,像数组一样使用。

5.System V消息队列

消息队列的基本原理

消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成,两个互相通信的进程通过某种方式看到同一个消息队列,这两个进程向对方发数据时,都在消息队列的队尾添加数据块,这两个进程获取数据块时,都在消息队列的队头取数据块。


注意消息队列发送的是有类型数据块

消息队列的类型(type),就是给消息打标签。

发送时指定类型,接收时只拿对应类型,用来区分消息用途、避免乱拿。

消息队列数据结构

当然,系统当中也可能会存在大量的消息队列,系统一定也要为消息队列维护相关的内核数据结构。

bash 复制代码
struct msqid_ds {
	struct ipc_perm msg_perm;
	struct msg *msg_first;      /* first message on queue,unused  */
	struct msg *msg_last;       /* last message in queue,unused */
	__kernel_time_t msg_stime;  /* last msgsnd time */
	__kernel_time_t msg_rtime;  /* last msgrcv time */
	__kernel_time_t msg_ctime;  /* last change time */
	unsigned long  msg_lcbytes; /* Reuse junk fields for 32 bit */
	unsigned long  msg_lqbytes; /* ditto */
	unsigned short msg_cbytes;  /* current number of bytes on queue */
	unsigned short msg_qnum;    /* number of messages in queue */
	unsigned short msg_qbytes;  /* max number of bytes on queue */
	__kernel_ipc_pid_t msg_lspid;   /* pid of last msgsnd */
	__kernel_ipc_pid_t msg_lrpid;   /* last receive pid */
};

可以看到消息队列数据结构的第一个成员是msg_perm ,它和共享内存 的数据结构的第一个成员shm_perm是同一个类型的结构体变量,ipc_perm结构体的定义如下:

cpp 复制代码
struct ipc_perm{
	__kernel_key_t  key;
	__kernel_uid_t  uid;
	__kernel_gid_t  gid;
	__kernel_uid_t  cuid;
	__kernel_gid_t  cgid;
	__kernel_mode_t mode;
	unsigned short  seq;
};

消息队列的创建

创建消息队列我们需要用msgget函数,msgget函数的函数原型如下:

bash 复制代码
int msgget(key_t key, int msgflg);

1.创建消息队列也需要使用ftok函数生成一个key值,这个key值作为msgget函数的第一个参数。

2.msgget函数的第二个参数,与创建共享内存时使用的shmget函数的第三个参数相同。

3.消息队列创建成功时,msgget函数返回的一个有效的消息队列标识符(用户层标识符)。

消息队列的释放

释放消息队列我们需要用msgctl函数,msgctl函数的函数原型如下:

bash 复制代码
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

msgctl函数的参数与释放共享内存时使用的shmctl函数的三个参数相同,只不过msgctl函数的第三个参数传入的是消息队列的相关数据结构。

向消息队列发送数据

向消息队列发送数据我们需要用msgsnd函数,msgsnd函数的函数原型如下:

bash 复制代码
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

msgsnd函数的参数说明:

第一个参数msqid,表示消息队列的用户级标识符。

第二个参数msgp,表示待发送的数据块。

第三个参数msgsz,表示所发送数据块的大小

第四个参数msgflg,表示发送数据块的方式,一般默认为0即可。

msgsnd函数的返回值说明:

msgsnd调用成功,返回0。

msgsnd调用失败,返回-1。

其中msgsnd函数的第二个参数必须为以下结构:

cpp 复制代码
struct msgbuf{
	long mtype;       /* message type, must be > 0 */
	char mtext[1];    /* message data */
};

注意: 该结构当中的第二个成员mtext即为待发送的信息,当我们定义该结构时,mtext的大小可以自己指定。

从消息队列获取数据

从消息队列获取数据我们需要用msgrcv函数,msgrcv函数的函数原型如下:

bash 复制代码
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msgrcv函数的参数说明:

第一个参数msqid,表示消息队列的用户级标识符。

第二个参数msgp,表示获取到的数据块,是一个输出型 参数。

第三个参数msgsz,表示要获取数据块的大小

第四个参数msgtyp,表示要接收数据块的类型。

msgrcv函数的返回值说明:

msgsnd调用成功,返回实际获取到mtext数组中的字节数。

msgsnd调用失败,返回-1。

6.System V信号量

并发编程,概念铺垫

• 多个执⾏流(进程),能看到的同⼀份公共资源:共享资源

• 被保护起来的共享资源叫做临界资源

• 保护的⽅式常⻅:互斥与同步

• 任何时刻,只允许⼀个执⾏流访问资源,叫做互斥

• 多个执⾏流,访问临界资源的时候,具有⼀定的顺序性,叫做同步

• 系统中某些资源⼀次只允许⼀个进程使⽤,称这样的资源为临界资源或互斥资源。

• 在进程中涉及到互斥资源的程序段叫临界区。你写的代码=访问临界资源的代码(临界区)+不访问

临界资源的代码(⾮临界区)

• 所谓的对共享资源进⾏保护,本质是对访问共享资源的代码进⾏保护

在系统当中也为信号量维护了相关的内核数据结构。

信号量的数据结构如下:

cpp 复制代码
struct semid_ds {
	struct ipc_perm sem_perm;       /* permissions .. see ipc.h */
	__kernel_time_t sem_otime;      /* last semop time */
	__kernel_time_t sem_ctime;      /* last change time */
	struct sem  *sem_base;      /* ptr to first semaphore in array */
	struct sem_queue *sem_pending;      /* pending operations to be processed */
	struct sem_queue **sem_pending_last;    /* last pending operation */
	struct sem_undo *undo;          /* undo requests on this array */
	unsigned short  sem_nsems;      /* no. of semaphores in array */
};

信号量数据结构的第一个成员也是ipc_perm类型的结构体变量,ipc_perm结构体的定义如下:

cpp 复制代码
struct ipc_perm{
	__kernel_key_t  key;
	__kernel_uid_t  uid;
	__kernel_gid_t  gid;
	__kernel_uid_t  cuid;
	__kernel_gid_t  cgid;
	__kernel_mode_t mode;
	unsigned short  seq;
};

创建信号量集我们需要用semget函数,semget函数的函数原型如下:

bash 复制代码
int semget(key_t key, int nsems, int semflg);

创建信号量集也需要使用ftok函数生成一个key值,这个key值作为semget函数的第一个参数。

semget函数的第二个参数nsems,表示创建信号量的个数。

semget函数的第三个参数,与创建共享内存时使用的shmget函数的第三个参数相同。

信号量集创建成功时,semget函数返回的一个有效的信号量集标识符(用户层标识符)。

信号量保护公共资源简单原理

当进程A申请访问共享内存资源时,如果此时sem为1(sem代表当前信号量个数),则进程A申请资源成功,此时需要将sem减减,然后进程A就可以对共享内存进行一系列操作,但是在进程A在访问共享内存时,若是进程B申请访问该共享内存资源,此时sem就为0了,那么这时进程B会被挂起,直到进程A访问共享内存结束后将sem加加,此时才会将进程B唤起,然后进程B再对该共享内存进行访问操作。

7.system V IPC联系

通过对system V系列进程间通信的学习,可以发现共享内存、消息队列以及信号量,虽然它们内部的属性差别很大,但是维护它们的数据结构的第一个成员确实一样的,都是ipc_perm类型的成员变量。

c实现多态

Linux 系统中,共享内存、消息队列、信号量这三种 IPC 对象,它们在内核里对应的数据结构第一个成员都是相同的公共结构体,相当于面向对象里的基类,内核只需要用数组或哈希表统一管理这个公共部分,就能统一维护权限、key 等通用信息;当需要操作具体的 IPC 对象时,再把公共结构体的指针强转为对应具体类型的指针,就可以访问各自独有的数据和功能。

相关推荐
wanhengidc1 小时前
服务器硬盘都有哪些功能
大数据·运维·服务器·数据库·科技
认真的柯南1 小时前
深入解析服务器内存架构:从DRAM颗粒到NUMA模式
服务器·架构·numa
wanhengidc1 小时前
服务器分布式存储的功能
运维·服务器·分布式
w1225h1 小时前
Nginx环境安装
运维·nginx
徐子元竟然被占了!!2 小时前
Linux的less
linux·运维·less
Vect__2 小时前
muduo网络服务器项目篇:服务器模块设计
运维·服务器·网络
坚持就完事了2 小时前
Linux文件路径
linux·运维·服务器
啊哈哈121382 小时前
计算机三级备考(七)——高级数据库查询
服务器·数据库
新缸中之脑2 小时前
用Gws+Valyu实现晨报自动化
运维·自动化·php