【Linux篇】第十二篇——进程间通信(管道+system V共享内存)(三)

简介: 【Linux篇】第十二篇——进程间通信(管道+system V共享内存)

system V进程间通信


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

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

  1. system V共享内存
  2. system V消息队列
  3. system V信号量
  4. 其中,system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴。

说明一下:

system V共享内存和system V消息队列就类似于手机,用于沟通信息。system V信号量就类似于下棋比赛时用的棋钟,用于保证两个棋手之间的同步与互斥。

system V共享内存


共享内存的基本原理


共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。

image.png

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

共享内存数据结构


在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。

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

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结构体的定义如下:

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;
};

共享内存的建立与释放


共享内存的建立大致包括以下两个过程:

  1. 在物理内存当中申请共享内存空间。
  2. 将申请到的共享内存挂接到地址空间,即建立映射关系。
  3. 共享内存的释放大致包括以下两个过程:
  4. 将共享内存与地址空间去关联,即取消映射关系。
  5. 释放共享内存空间,即将物理内存归还给系统。

共享内存的创建


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

int shmget(key_t key,size_t size,int shmflg);

shmget函数的参数说明:

  • 第一个参数Key,表示待创建共享内存在系统当中的唯一标识
  • 第二个参数size,表示待创建共享内存的大小。
  • 第三个参数shmflg,表示创建共享内存的方式。
  • shmget函数的返回值说明:
  • shmget调用成功,返回一个有效的共享内存标识符(用户层标识符)。
  • shmget调用失败,返回-1。

注意:


我们把具有标定某种资源能力的东西叫做句柄,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作 。

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

注意:


我们把具有标定某种资源能力的东西叫做句柄,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作 。


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

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


传入shmget函数的第三个参数shmflg,常用的组合方式有以下两种:

组合方式 作用
IPC_CREAT 如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则直接返回该共享内存的句柄。
IPC_CREAT|IPC_EXCL 如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则出错返回

换句话说:

  • 使用组合IPC_CREAT | IPC_EXCL,只有shmget函数调用成功时才会获得共享内存的句柄,并且该共享内存一定是新建的共享内存。
  • 使用组合IPC_CREAT,一定会获得一个共享内存的句柄,但无法确认该共享内存是否是新建的共享内存。

至此我们就可以使用ftok和shmget函数创建一块共享内存了,创建后我们可以将共享内存的key值和句柄进行打印,以便观察,代码如下:

#include <stdio.h>
#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <unistd.h>
#define PATHNAME "./" //路径名
#define PROJ_ID 0x6666 //整数标识符
#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); //打印key值
  printf("shm: %d\n", shm); //打印句柄
  return 0;
}

运行结果:

image.png

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

image.png

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

  • -q:列出消息队列相关信息
  • -m;列出共享内存相关信息
  • -s:列出信号量相关信息。

这里携带-m选项查看共享内存相关信息:

image.png

ipcs命令输出的每列信息含义如下:

标题 含义
key 系统区别各个共享内存的唯一标识
shmid

共享内存的用户层id

owner

共享内存的拥有者
perms 共享内存的权限
nattch

关联共享内存的进程数

status 共享内存的状态

注意:key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系类似于fd和FILE*之间的关系。

共享内存的释放


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


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


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

使用命令释放共享内存资源

image.png

注意:指定删除时使用的是共享内存的用户层id,即列表当中的shmid。

使用程序释放共享内存资源

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmctl函数的参数说明:

  • 第一个参数shmid,表示所控制共享内存的用户级标识符
  • 第二个参数cmd,表示具体的控制动作
  • 第三个参数buf,用于获取或设置所控制共享内存的数据结构

shmctl函数的返回值说明:

  • shmctl调用成功,返回0
  • shmctl调用失败,返回-1

其中,作为shmctl函数的第二个参数传入的常用选项有以下三个:

  • shmctl调用成功,返回0
  • shmctl调用失败,返回-1

其中,作为shmctl函数的第二个参数传入的常用选项有以下三个:

选项 作用
IPC_STAT 获取共享内存的当前关联值,此时参数buf作为输出型参数
IPC_RMID 删除共享内存段
IPC_SET 在进程有足够权限的前提下,将共享内存当前关联值设置为buf所指的数据结构中的值

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

我们可以在程序运行时,使用以下监控脚本时刻关注共享内存的资源分配情况

while :; do ipcs -m;echo "###################################";sleep 1;done

通过监控脚本可以确定共享内存确实创建并且成功释放了。

image.png

共享内存的关联


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

void *shmat(int shmid, const void *shmaddr, int shmflg);

shmat函数参数说明:

  • 第一个参数shmid,表示待关联共享内存的用户级标识符
  • 第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置
  • 第三个参数shmflg,表示关联共享内存时设置的某些属性

shmat函数的返回值说明:

  • shmat调用成功,返回共享内存映射到进程地址空间中的起始地址
  • shmat调用失败,返回-1.

其中,作为shmat函数的第三个参数传入的常用选项有以下三个:

选项 作用
SHM_RDONLY 关联共享内存后只进行读取操作
SHM_RND 若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式为:shmaddr-(shmaddr%SHMLBA)
0 默认为读写权限

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

#include <stdio.h>
#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <unistd.h>
#define PATHNAME "./" //路径名
#define PROJ_ID 0x6666 //整数标识符
#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); //打印key值
  printf("shm: %d\n", shm); //打印句柄
    printf("attach begin!\n");
  sleep(2);
  char* mem = shmat(shm, NULL, 0); //关联共享内存
  if (mem == (void*)-1){
    perror("shmat");
    return 1;
  }
  printf("attach end!\n");
  sleep(2);
  shmctl(shm, IPC_RMID, NULL); //释放共享内存
  return 0;
}

运行结果如下所示,发现关联失败,主要原因是我们使用shmget函数创建共享内存时,并没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即没有任何权限。

image.png

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

int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建权限为0666的共享内存

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

image.png

共享内存的去关联


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

int shmdt(const void *shmaddr);

shmdt函数参数的说明:

  • 待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。

shmdt函数的返回值说明:

  • shmdt调用成功,返回0
  • shmdt调用失败,返回-1

现在我们能够取消共享内存与进程之间的关联了

#include <stdio.h>
#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <unistd.h>
#define PATHNAME "./" //路径名
#define PROJ_ID 0x6666 //整数标识符
#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); //打印key值
  printf("shm: %d\n", shm); //打印句柄
    printf("attach begin!\n");
  sleep(2);
  char* mem = shmat(shm, NULL, 0); //关联共享内存
  if (mem == (void*)-1){
    perror("shmat");
    return 1;
  }
  printf("attach end!\n");
  sleep(2);
  printf("detach begin!\n");
  sleep(2);
  shmdt(mem); //共享内存去关联
  printf("detach end!\n");
  sleep(2);
  shmctl(shm, IPC_RMID, NULL); //释放共享内存
  return 0;
}

运行程序,通过监控即可发现该共享内存的关联数由1变为0的过程,即取消了共享内存与该进程之间的关联。

image.png

注意:将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的练习。

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


在知道了共享内存的创建,关联,去关联以及释放后,现在可以尝试让两个进程通过共享内存进行通信了。在让两个进程进行通信之前,我们可以先测试一下这两个进程能否成功挂接同一个共享内存上。


服务端负责创建共享内存,创建好后将共享内存和服务端进行关联,之后进入死循环,便于观察服务端是否挂接成功。


服务端代码如下:

//server.c
#include "comm.h"
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 | 0666); //创建新的共享内存
  if (shm < 0){
    perror("shmget");
    return 2;
  }
  printf("key: %x\n", key); //打印key值
  printf("shm: %d\n", shm); //打印共享内存用户层id
  char* mem = shmat(shm, NULL, 0); //关联共享内存
  while (1){
    //不进行操作
  }
  shmdt(mem); //共享内存去关联
  shmctl(shm, IPC_RMID, NULL); //释放共享内存
  return 0;
}

客户端只需要直接和服务端创建的共享内存进行关联即可,之后也进入死循环,便于观察客户端是否挂接成功。

客户端代码如下:

//client.c
#include "comm.h"
int main()
{
  key_t key = ftok(PATHNAME, PROJ_ID); //获取与server进程相同的key值
  if (key < 0){
    perror("ftok");
    return 1;
  }
  int shm = shmget(key, SIZE, IPC_CREAT); //获取server进程创建的共享内存的用户层id
  if (shm < 0){
    perror("shmget");
    return 2;
  }
  printf("key: %x\n", key); //打印key值
  printf("shm: %d\n", shm); //打印共享内存用户层id
  char* mem = shmat(shm, NULL, 0); //关联共享内存
  int i = 0;
  while (1){
    //不进行操作
  }
  shmdt(mem); //共享内存去关联
  return 0;
}

为了让服务端和客户端在使用ftok函数获取key值时,能够得到同一种key值,那么服务端和客户端传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享资源进行挂接。这里我们可以将这些需要共用的信息放入一个头文件当中,服务端和客户端共用这个头文件即可。


共用头文件的代码如下:

//comm.h
#include <stdio.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小

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

image.png

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

客户端不断向共享内存写入数据:

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

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

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

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

image.png

共享内存与管道进行对比


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

我们先来看看管道通信:

image.png

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

  • 服务端将信息从输入文件复制到服务端的临时缓冲区中
  • 将服务端临时缓冲区的信息复制到管道中
  • 客户端将信息从管道复制到客户端的缓冲区
  • 将客户端临时缓冲区的信息复制到输出文件中

我们再来看看共享内存通信:

image.png

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

  • 从输入文件到共享内存。
  • 从共享内存到输出文件。

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

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

System V消息队列


消息队列的基本原理


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

image.png

其中消息队列当中的某一个数据块是由谁发送给谁的,取决于数据块的类型。

总结一下:

  • 消息队列提供了一个从一个进程向另一个进程发送数据块的方法。
  • 和共享内存一样,消息队列的资源也必须自行删除,否则不会自动清除,因为system V IPC资源的生命周期是随内核的。

消息队列数据结构


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

消息队列的数据结构如下:

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结构体的定义如下

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函数的函数原型如下:

int msgget(key_t key, int msgflg);

说明一下:

  • 创建消息队列也需要使用ftok函数生成一个key值,这个key值作为msgget函数的第一个参数
  • msgget函数的第二个参数,与创建共享内存时使用的shmget函数的第三个参数相同。
  • 消息队列创建成功时,msgget函数返回的一个有效的消息队列标识符(用户层标识符)。

消息队列的释放


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

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

说明一下:

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

向消息队列发送数据


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

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

msgsnd函数的参数说明:

  • 第一个参数msqid,表示消息队列的用户级标识符。
  • 第二个参数msgp,表示待发送的数据块。
  • 第三个参数msgsz,表示所发送数据块的大小
  • 第四个参数msgflg,表示发送数据块的方式,一般默认为0即可

msgsnd函数的返回值说明:

  • msgsnd调用成功,返回0。
  • msgsnd调用失败,返回-1。

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

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

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

从消息队列获取数据


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

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msgrcv函数的参数说明

  • 第一个参数msqid,表示消息队列的用户级标识符。
  • 第二个参数msgp,表示获取到的数据块,是一个输出型参数。
  • 第三个参数msgsz,表示要获取数据块的大小
  • 第四个参数msgtyp,表示要接收数据块的类型。
  • msgrcv函数的返回值说明:
  • msgsnd调用成功,返回实际获取到mtext数组中的字节数。
  • msgsnd调用失败,返回-1。

System V信号量


信号量相关概念


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

信号量数据结构


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

信号量的数据结构如下:

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结构体的定义如下

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函数的函数原型如下:

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

说明一下:

  1. 创建信号量集也需要使用ftok函数生成一个key值,这个key值作为semget函数的第一个参数
  2. semget函数的第二个参数nsems,表示创建信号量的个数
  3. semget函数的第三个参数,与创建共享内存时使用的shmget函数的第三个参数相同。
  1. 信号量集创建成功时,semget函数返回的一个有效的信号量集标识符(用户层标识符)

信号量集的删除

删除信号量集我们需要用semctl函数,semctl函数的函数原型如下:

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

信号量集的操作

对信号量集进行操作我们需要用semop函数,semop函数的函数原型如下:

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

进程互斥


进程间通信通过共享资源来实现,这虽然解决了通信的问题,但是也引入了新的问题,那就是通信进程间共用的临界资源,若是不对临界资源进行保护,就可能产生各个进程从临界资源获取的数据不一致等问题。


保护临界资源的本质是保护临界区,我们把进程代码中访问临界资源的代码称之为临界区,信号量就是用来保护临界区的,信号量分为二元信号量和多元信号量。


比如当前有一块大小为100字节的资源,我们若是以25字节为一份,那么该资源可以被分为4份,那么此时这块资源可以由4个信号量进行标识。

image.png

信号量本质是一个计数器,在二元信号量中,信号量的个数为1(相当于将临界资源看成一整块),二元信号量本质解决了临界资源的互斥问题,以下面的伪代码进行解释:

image.png

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

在这种情况下,无论什么时候都只会有一个进程在对同一份共享内存进行访问操作,也就解决了临界资源的互斥问题。


实际上,代码中计数器sem减减的操作就叫做P操作,而计数器加加的操作就叫做V操作,P操作就是申请信号量,而V操作就是释放信号量。

image.png

System V IPC联系


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

这样设计的好处就是,在操作系统内可以定义一个struct ipc_perm类型的数组,此时每当我们申请一个IPC资源,就在该数组当中开辟一个这样的结构。


也就是说,在内核当中只需要将所有的IPC资源的ipc_perm成员组织成数组的样子,然后用切片的方式获取到该IPC资源的起始地址,然后就可以访问该IPC资源的每一个成员了。

相关文章
|
12天前
|
C语言 开发者 内存技术
探索操作系统核心:从进程管理到内存分配
本文将深入探讨操作系统的两大核心功能——进程管理和内存分配。通过直观的代码示例,我们将了解如何在操作系统中实现这些基本功能,以及它们如何影响系统性能和稳定性。文章旨在为读者提供一个清晰的操作系统内部工作机制视角,同时强调理解和掌握这些概念对于任何软件开发人员的重要性。
|
10天前
|
算法 Linux
深入探索Linux内核的内存管理机制
本文旨在为读者提供对Linux操作系统内核中内存管理机制的深入理解。通过探讨Linux内核如何高效地分配、回收和优化内存资源,我们揭示了这一复杂系统背后的原理及其对系统性能的影响。不同于常规的摘要,本文将直接进入主题,不包含背景信息或研究目的等标准部分,而是专注于技术细节和实际操作。
|
12天前
|
Linux 调度 C语言
深入理解操作系统:从进程管理到内存优化
本文旨在为读者提供一次深入浅出的操作系统之旅,从进程管理的基本概念出发,逐步探索到内存管理的高级技巧。我们将通过实际代码示例,揭示操作系统如何高效地调度和优化资源,确保系统稳定运行。无论你是初学者还是有一定基础的开发者,这篇文章都将为你打开一扇了解操作系统深层工作原理的大门。
|
23天前
|
缓存 Ubuntu Linux
Linux环境下测试服务器的DDR5内存性能
通过使用 `memtester`和 `sysbench`等工具,可以有效地测试Linux环境下服务器的DDR5内存性能。这些工具不仅可以评估内存的读写速度,还可以检测内存中的潜在问题,帮助确保系统的稳定性和性能。通过合理配置和使用这些工具,系统管理员可以深入了解服务器内存的性能状况,为系统优化提供数据支持。
29 4
|
21天前
|
算法 调度 开发者
深入理解操作系统:从进程管理到内存分配
本文旨在为读者提供一个深入浅出的操作系统知识之旅,从进程管理的基础概念出发,探索内存分配的策略与技巧。我们将通过实际代码示例,揭示操作系统背后的逻辑与奥秘,帮助读者构建起对操作系统工作原理的直观理解。文章不仅涵盖理论知识,还提供实践操作的指导,使读者能够将抽象的概念转化为具体的技能。无论你是初学者还是有一定基础的开发者,都能在这篇文章中找到有价值的信息和启发。
|
25天前
|
算法 调度 C++
深入理解操作系统:从进程管理到内存分配
【10月更文挑战第42天】本文将带你进入操作系统的神秘世界,探索其核心概念和关键技术。我们将从进程管理开始,了解操作系统如何协调和管理多个程序的运行;然后,我们将深入研究内存分配,看看操作系统如何有效地分配和管理计算机的内存资源。通过这篇文章,你将获得对操作系统工作原理的深入理解,并学会如何编写高效的代码来利用这些原理。
|
18天前
|
存储 算法 安全
深入理解Linux内核的内存管理机制
本文旨在深入探讨Linux操作系统内核的内存管理机制,包括其设计理念、实现方式以及优化策略。通过详细分析Linux内核如何处理物理内存和虚拟内存,揭示了其在高效利用系统资源方面的卓越性能。文章还讨论了内存管理中的关键概念如分页、交换空间和内存映射等,并解释了这些机制如何协同工作以提供稳定可靠的内存服务。此外,本文也探讨了最新的Linux版本中引入的一些内存管理改进,以及它们对系统性能的影响。
|
Linux Shell
详解linux进程间通信-管道 popen函数 dup2函数
  前言:进程之间交换信息的唯一方法是经由f o r k或e x e c传送打开文件,或通过文件系统。本章将说明进程之间相互通信的其他技术—I P C(InterProcess Communication)。
1598 0
|
28天前
|
Linux 网络安全 数据安全/隐私保护
Linux 超级强大的十六进制 dump 工具:XXD 命令,我教你应该如何使用!
在 Linux 系统中,xxd 命令是一个强大的十六进制 dump 工具,可以将文件或数据以十六进制和 ASCII 字符形式显示,帮助用户深入了解和分析数据。本文详细介绍了 xxd 命令的基本用法、高级功能及实际应用案例,包括查看文件内容、指定输出格式、写入文件、数据比较、数据提取、数据转换和数据加密解密等。通过掌握这些技巧,用户可以更高效地处理各种数据问题。
69 8
|
28天前
|
监控 Linux
如何检查 Linux 内存使用量是否耗尽?这 5 个命令堪称绝了!
本文介绍了在Linux系统中检查内存使用情况的5个常用命令:`free`、`top`、`vmstat`、`pidstat` 和 `/proc/meminfo` 文件,帮助用户准确监控内存状态,确保系统稳定运行。
186 6