首页 > 系统相关 >linux下进程间通信

linux下进程间通信

时间:2023-08-19 11:44:26浏览次数:49  
标签:信号量 include int 间通信 key linux 进程 共享内存

进程间通信

一、进程间通信的介绍

1、进程间通信的概念

进程通信(Interprocess communication),简称:IPC;
本来进程之间是相互独立的。但是由于不同的进程之间可能要共享某些信息,所以就必须要有通讯来实现进程间的互斥和同步。比如说共享同一块内存、管道、消息队列、信号量等等就是实现这一过程的手段,相当于移动公司在打电话的作用。

2、进程间通信的目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

3、进程间通信的前提

​ 进程间通信的前提本质:由操作系统参与,提供一份所有通信进行都能看到的公共资源;两个或多个进程相互通信,必须先看到一份公共的资源,这里的所谓的资源是属于操作系统的,就是一段内存(可能以文件的方式提供、可能以队列的方式提供,也有可能提供的就是原始内存块),这也就是通信方式有很多种的原因;

4、进程间通信的分类

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存(重点介绍)
  • System V 信号量

POSIX IPC(本次不做介绍)

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

二、管道

管道是Unix中最古老的进程间通信的形式。

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

三、匿名管道

1、基本原理

匿名管道用于进程间通信,且仅限于父子进程之间的通信。

我们知道进程的PCB中包含了一个指针数组 struct file_struct,它是用来描述并组织文件的。父进程和子进程均有这个指针数组,因为子进程是父进程的模板,其代码和数据是一样的;

打开一个文件时,其实是将文件加载到内核中,内核将会以结构体(struct file)的形式将文件的相关属性、文件操作的指针集合(即对应的底层IO设备的调用方法)等;

当父进程进行数据写入时(例如:写入“hello Linux”),数据是先被写入到用户级缓冲区,经由系统调用函数,又写入到了内核缓冲区,在进程结束或其他的操作下才被写到了对应的设备中;

如果数据在写入设备之前,“hello Linux”是在内核缓冲区的,因为子进程和父进程是同时指向这个文件的,所以子进程是能够看到这个数据的,并且可以对其操作;

简单来说,父进程向文件写入数据时,不直接写入对应的设备中,而是将数据暂存在内核缓冲区中,交给子进程来处理;

所以这种基于文件的方式就叫做管道;

2、管道的创建步骤

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

匿名管道属于单向通信,意味着父子进程只有一个端是打开的,实现父子通信的时候就需要根据自己的想要实现的情况,关闭对应的文件描述符;

1、pipe函数

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

函数的参数是两个文件的描述符,是****输出型参数:

  • pipefd[0]:读管道 --- 对应的文件描述符是3
  • pipefd[1]:写管道 --- 对应的文件描述符是4

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

#include <stdio.h>
#include <unistd.h>                                                                                                          
#include <stdlib.h>
#include <string.h>
 
int main()
{
    int pipefd[2] = {0};
    if(pipe(pipefd) != 0){
        perror("pipe error!");
        return 1;
    }
 
    //pipefd[0]:读取段  pipefd[1]:写入端
    printf("pipefd[0]:%d\n",pipefd[0]);//3
    printf("pipefd[1]:%d\n",pipefd[1]);//4
    return 0;
}

2、代码实战

接下来我们来实现子进程写入数据,父进程读取数据;那么我们就需要针对父子进程关闭对应的文件描述符fd,子进程关闭读端,父进程关闭写端;**

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>                                                                                                          
#include <string.h>
 
//让子进程sleep
int main()
{
    int pipefd[2] = {0};
    if(pipe(pipefd) != 0){ //创建匿名管道
        perror("pipe error!");
        return 1;
    }
 
    //pipefd[0]:读取端  pipefd[1]:写入端
    printf("pipefd[0]:%d\n",pipefd[0]);//3
    printf("pipefd[1]:%d\n",pipefd[1]);//4
 
    if(fork() == 0){
        //子进程---写入
        close(pipefd[0]); //关闭子进程的读取端
        const char* msg = "hello-linux!";
        while(1){
            write(pipefd[1], msg, strlen(msg)); //子进程不断的写数据
            sleep(1);
        }
    exit(0);
    }
 
    //父进程---读取
    close(pipefd[1]); //关闭父进程的写入端
    char buffer[64] = {0};
    while(1){
        //如果read返回值是0,就意味着子进程关闭文件描述符了
        ssize_t s = read(pipefd[0], buffer, sizeof(buffer)); //父进程不断的读数据
        if(s == 0){
            break;
        }
        else if(s > 0){
            buffer[s] = 0;
            printf("child say to father:%s\n",buffer);
        }
        else{
            break;
        }
    }
	return 0;
}

3、管道的五个特点和四种情况

五个特点:

  1. 管道是一个只能单向通信的通信信道,仅限于父子间通信
  2. 管道提供流式服务
  3. 管道操作自带同步与互斥机制
  4. 进程退出,管道释放,所以管道的生命周期随进程
  5. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

四种情况:

  1. 读端不读或者读的慢,写端要等待读端;
  2. 读端关闭,写端收到SIGPIPE信号直接终止;
  3. 写端不写或者写的慢,读端要等待写端;
  4. 写端关闭,读端读完pipe内部的数据然后再读,会读到0为止,表明读到文件结尾;

接下来我们通过下面的程序进行验证 :管道是单向通信和面向字节流

具体验证请参考文章末尾链接

4、管道的读写规则

int pipe(int pipefd[2]);
int pipe2(int pipefd[2], int flags);

当没有数据可读时:

  • 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将不再保证写入的原子性。

四、命名管道

匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。 命名管道是一种特殊类型的文件;

1、命名管道的创建

1、命名行创建

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

mkfifo myfifo

我们创建好命令管道后,就可以实现两个进程间的通信了;(左图的进程进行循环的数据写入,右图进程进行读取)当我们关闭读端的时候,写端也会被操作系统关闭,当我们关闭写端时,读端会一直在等写端;

当然也可以让读端不断的读取数据,写端只要写就行了()

2、程序创建(mkfifo函数)

在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:

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

参数说明:

  • pathname:表示你要创建的命名管道文件。
    • 如果pathname是以文件的方式给出,默认在当前的路径下创建;
    • 如果pathname是以某个路径的方式给出,将会在这个路径下创建;
  • mode:表示给创建的命名管道设置权限。
    • 我们在设置权限时,例如0666权限,它会受到系统的umask(文件默认掩码)的影响,实际创建出来是(mode & ~umask)0664;所以想要正确的得到自己设置的权限(0666),我们需要将文件默认掩码设置为0;

返回值:

  • 命名管道创建成功返回0,失败返回-1
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
 
#define MY_FIFO "myfifo"      //默认是在当前路径下创建
//#define MY_FIFO "../xxx/myfifo" //指定在上级目录下的xxx目录下创建
 
int main()
{
    umask(0);                                                                                                                           
    if(mkfifo(MY_FIFO, 0666) < 0)
    {
         perror("mkfifo");
         return 1;
    }
    return 0;
}

2、命名管道的打开规则

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

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
  • O_NONBLOCK enable:立刻返回成功

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

  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

3、用命名管道实现server&client通信

实现server(服务端)和client(客户端)之间的通信,我们让server创建命名管道,用来读取命名管道内的数据;client获取管道,用来向命名管道内写数据;server(服务端)和client(客户端)想要使用同一个管道,这里我们可以让客户端和服务端包含同一个头文件comm.h,该头文件当中提供这个共用的命名管道文件的文件名,这样客户端和服务端就可以通过这个文件名,打开同一个命名管道文件,进而进行通信了。

comm.h:

#pragma once
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>                                              
#define MY_FIFO "./fifo" 

server.c:

#include "comm.h"
int main()
{
 umask(0); //将文件掩码设置为0,确保得到我们设置的权限
 if(mkfifo(MY_FIFO, 0666) < 0){ //服务端用来创建命名管道文件
     perror("mkfifo");
     return 1;
 }

 int fd = open(MY_FIFO, O_RDONLY); //以只读的方式打开命名管道文件
 if(fd < 0){
     perror("open");
     return 2;
 }
 while(1){
     char buffer[64] = {0};
     ssize_t s = read(fd, buffer, sizeof(buffer) - 1); //从fd(命名管道)中读数据到buffer中
     if(s > 0){                                                                                                                 
         buffer[s] = 0;
         printf("client: %s\n", buffer); //打印客户端发来的数据
     }
     else if(s == 0){
         printf("client qiut...\n");
         break;
     }
     else{
         perror("open");
         break;
     }
 }
 close(fd); //通信结束,关闭命名管道文件
 return 0;
} 

client.c:

#include "comm.h"

int main()
{
 //这里不需要创建fifo,只需要获取就行
 int fd = open(MY_FIFO, O_WRONLY); //以写的方式打开命名管道文件
 if(fd < 0){ 
     perror("open");
     return 1;
 }
 //业务逻辑
 while(1){
     printf("请输入:");
     fflush(stdout);
     char buffer[64] = {0};
     //先把数据从标准输入拿到我们的client进程内部
     ssize_t s = read(0, buffer, sizeof(buffer) - 1);
     if(s > 0){
         buffer[s-1] = 0;
         printf("%s\n",buffer);
         //拿到了数据,将数据写入命名管道
         write(fd, buffer, strlen(buffer));
     }
 }
 close(fd); //通信完毕,关闭命名管道文件
 return 0;
}

编写Makefile:

.PHONY:all
all:client server
client:client.c
	gcc -o $@ $^

server:server.c
	gcc -o $@ $^


.PHONY:clean
clean:
	rm -rf client server fifo

接下来使用Makefile进行编译,然后我们需要先将服务端运行起来,再运行客户端,因为服务端是用来创建命名管道文件的,先运行客户端的话,是不可以打开一个不存在的文件的;

4、用命名管道实现client控制server执行某种任务

两个进程间的通信,不是只能发送一些字符串,还可以实现一个进程控制另一个进程去完成某种任务;比如:client(客户端)向让server(服务端)执行“显示当前目录下的所有文件信息”的任务和执行“小火车命令sl”

#include "comm.h"
int main()
{
	umask(0); //将文件掩码设置为0,确保得到我们设置的权限
	if (mkfifo(MY_FIFO, 0666) < 0) { //服务端用来创建命名管道文件
		perror("mkfifo");
		return 1;
	}

	int fd = open(MY_FIFO, O_RDONLY); //以只读的方式打开命名管道文件
	if (fd < 0) {
		perror("open");
		return 2;
	}
	while (1) {
		char buffer[64] = { 0 };
		ssize_t s = read(fd, buffer, sizeof(buffer) - 1); //从fd(命名管道)中读数据到buffer中
		if (s > 0) {
			buffer[s] = 0;

         //client控制server完成某种动作/任务
			if (strcmp(buffer, "show") == 0) {
				if (fork() == 0) {
					execl("/usr/bin/ls", "ls", "-l", NULL);
					exit(1);
				}
				waitpid(-1, NULL, 0);
			}

			else if (strcmp(buffer, "run") == 0) {
				if (fork() == 0) {
					execl("/usr/bin/sl", "sl", NULL);
				}
			}

			else {
				printf("client: %s\n", buffer);
			}
		}

		else if (s == 0) {
			printf("client qiut...\n");
			break;
		}

		else {
			perror("open");
			break;
		}
	}
	close(fd); //通信结束,关闭命名管道文件
	return 0;
}

5、管道的总结

管道:

  • 管道分为匿名管道和命名管道;
  • 管道通信方式的中间介质是文件,通常称这种文件为管道文件;
  • 匿名管道:管道是半双工的,数据只能单向通信;需要双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。
  • 命名管道:不同于匿名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信
  • 利用系统调用pipe()创建一个无名管道文件,通常称为无名管道或PIPE;利用系统调用mkfifo()创建一个命名管道文件,通常称为有名管道或FIFO。
  • PIPE是一种非永久性的管道通信机构,当它访问的进程全部终止时,它也将随之被撤消。
  • FIFO是一种永久的管道通信机构,它可以弥补PIPE的不足。管道文件被创建后,使用open()将文件进行打开,然后便可对它进行读写操作,通过系统调用write()和read()来实现。通信完毕后,可使用close()将管道文件关闭。
  • 匿名管道的文件是内存中的特殊文件,而且是不可见的,命名管道的文件是硬盘上的设备文件,是可见的。

五、system V进程间通信

进程间通信(interprocess communication,简称 IPC)指两个进程之间的通信。系统中的每一个进程都有各自的地址空间,并且相互独立、隔离,每个进程都处于自己的地址空间中。所以同一个进程的不同模块譬如不同的函数)之间进行通信都是很简单的,譬如使用全局变量等。但是,两个不同的进程之间要进行通信通常是比较难的,因为这两个进程处于不同的地址空间中。
  Linux 内核提供了多种 IPC 机制,其中System V IPC 包括:System V 信号量、System V消息队列、System V 共享内存。这三种通信机制有很多相似之处。

它是操作系统层面上专门为进程间通信设计的一个方案,其通信方式包括如下三种:

  • system V共享内存
  • system V消息队列
  • system V信号量

其中共享内存和消息队列是以传输数据为目的的,信号量是为了保证进程间的同步与互斥而设计的;

共享内存 消息队列 信号量
创建 shmget msgget semget
开始操作 shmat:连接映射 msgsend:发送 semop:P操作
结束操作 shmdt:分离映射 msgrev:接受 semop:V操作
删除 shmctl msgctl semctl

1、ftok函数

系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到。函数原型:

key_t ftok( const char * fname, int id )

参数说明:

  • fname:就是指定的文件名(已经存在的文件名),一般使用当前目录
  • id:子序号。虽然是int类型,但是只使用8bits(1-255)
  • 返回值:消息队列使用的ID值。

2、system V共享内存

1、共享内存的基本原理(示意图)

不同的进程想要看到同一份资源,在操作系统内部,一定是通过某种调用,在物理内存当中申请一块内存空间,然后通过某种调用,让参与通信进程“挂接”到这份新开辟的内存空间上;其本质:将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些参与通信进程便可以看到了同一份物理内存,这块物理内存就叫做共享内存。

2、共享内存的数据结构

我们知道在操作系统中是存在大量的进程的,如果两两进程进程进行通信,就需要多个共享内存。既然共享内存在系统中存在多份,就一定要将这些不同的共享内存管理起来,即先描述,再组织;为了保证两个或多个进程能够看到它们的同一份共享内存,那么共享内存一定要有能够唯一标识性的ID,方便让不同的进程识别它们的同一份共享内存;这个所谓的ID一定是在共享内存的数据结构中;

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 */
};
/*
    shm_perm   成员储存了共享内存对象的存取权限及其它一些信息。
    shm_segsz  成员定义了共享的内存大小(以字节为单位) 。
    shm_atime  成员保存了最近一次进程连接共享内存的时间。
    shm_dtime  成员保存了最近一次进程断开与共享内存的连接的时间。
    shm_ctime  成员保存了最近一次 shmid_ds 结构内容改变的时间。
    shm_cpid   成员保存了创建共享内存的进程的 pid 。
    shm_lpid   成员保存了最近一次连接共享内存的进程的 pid。
    shm_nattch 成员保存了与共享内存连接的进程数目
*/

对于每个IPC对象,系统共用一个struct ipc_perm的数据结构来存放权限信息,以确定一个ipc操作是否可以访问该IPC对象。

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

3、共享内存相关函数总览

函数原型 头文件 功能
int shmget (key_t key, size_t size, int shmflg); #include <sys/ipc.h>
#include <sys/shm.h>
创建共享内存
key_t ftok (const char *pathname, int proj_id); #include <sys/types.h>
#include <sys/ipc.h>
获取key
*int shmctl (int shmid, int cmd, struct shmid_ds *buf);* *#include <sys/ipc.h>
#include <sys/shm.h>*
控制共享内存
void *shmat (int shmid, const void *shmaddr, int shmflg); #include <sys/types.h>
#include <sys/shm.h>
共享内存关联
int shmdt (const void *shmaddr); #include <sys/types.h>
#include <sys/shm.h>
共享内存去关联

4、共享内存的创建

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

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

函数说明:

  • 得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符

参数说明:

  • key

  • 与信号量的semget函数一样,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1。不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget函数的返回值),只有shmget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。

  • size:以字节为单位指定需要共享的内存容量,所有的内存分配操作都是以页为单位的。所以如果一段进程只申请一块只有一个字节的内存,内存也会分配整整一页(在32位下一页的缺省大小PACE_SIZE=4096字节);这样,新创建的共享内存的大小实际上是从size这个参数调整而来的页面大小。即如果 size为1至4096,则实际申请到的共享内存大小为4K(一页);4097到8192,则实际申请到的共享内存大小为8K(两页),依此类推。

  • shmflg:是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。

    • shmflg主要和一些标志有关。
      其中有效的包括IPC_CREAT和IPC_EXCL,它们的功能与open()的O_CREAT和O_EXCL相当。 
      
      IPC_CREAT 如果共享内存不存在,则创建一个共享内存,否则打开操作。 
      
      IPC_EXCL 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
      
      如果单独使用IPC_CREAT:
      shmget()函数要么返回一个已经存在的共享内存的标识符 ,要么返回一个新建的共享内存的标识符。
      
      如果将 IPC_CREAT和IPC_EXCL标志一起使用:
      shmget()将返回一个新建的共享内存的标识符;如果该共享内存已存在,或者返回-1。
      IPC_EXEL标志本身并没有太大的意义,但是和IPC_CREAT标志一起使用可以用来保证所得的对象是新建的,而不是打开已有的对象。
      

返回值:

  • 调用成功,返回一个有效的共享内存标识符。
  • 调用失败,返回-1,错误原因存于errno中。

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

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
//把从pathname导出的信息与proj_id的低序8位组合成一个整数IPC键,传给shmget函数的key

ftok函数的作用就是,将一个已存在的路径名pathname(此文件必须存在且可存取)和一个整数标识符proj_id转换成一个key值。在使用shmget函数创建共享内存时,首先要调用ftok函数获取这个key值,这个key值会被填充进维护共享内存的数据结构当中,作为共享内存的唯一标识。

结合上面的知识,我们就可以来创建共享内存了,代码如下:

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

#define PATH_NAME "./" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096      //共享内存的大小

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

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

 printf("key: %u  shmid: %d\n", key, shmid);
 return 0;
}

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

ipcs

这里的key和上面打印出来的key是一样的,我们是以 无符号数10进制打印的;

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

其中:

  • key:共享内存的唯一键值
  • shmid:共享内存的编号
  • owner:创建的用户
  • perms:共享内存的权限
  • bytes:共享内存的大小
  • nattach:连接到共享内存的进程数
  • status:共享内存的状态

key vs shmid

key:只是用来在系统层面上进行标识唯一性的,不能用来管理共享内存;

shmid:是操作系统给用户返回的id,用来在用户层上进行管理共享内存;

key和shmid之间的关系类似于 fd 和 FILE* 之间的的关系。

5、共享内存的释放

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

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

  **此时我们若是要将创建的共享内存释放,有两个方法,一就是使用命令释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放。**
1、使用命令释放
[mlg@VM-20-8-centos shared_memory]$ ipcrm -m 5
//指定删除时使用的是共享内存的用户层id,即列表当中的shmid,不能使用key删除
2、使用函数释放

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

#include <sys/types.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

函数说明:完成对共享内存的控制,即释放共享内存

参数说明:

  • shmid:共享内存标识符

  • cmd:表示具体的控制动作

    • IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
    • IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
    • IPC_RMID:删除共享内存段
  • buf:共享内存管理结构体(参考上文的共享内存的数据结构)

    • struct shmid_ds
      {
          uid_t shm_perm.uid;
          uid_t shm_perm.gid;
          mode_t shm_perm.mode;
      };
      

返回值:

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

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

IPC_STAT 将信息从与shmid相关联的内核数据结构复制到buf指向的shmid_ds结构中。调用者必须具有共享内存段的读权限。
IPC_SET 改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
IPC_RMID 删除这片共享内存
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
 
#define PATH_NAME "./" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096      //共享内存的大小
 
int main()
{
    key_t key = ftok(PATH_NAME, PROJ_ID);//获取key值
    if(key < 0){
        perror("ftok");
        return 1;
    }
 
    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);//创建共享内存
    if(shmid < 0){
        perror("shmget");
        return 2;
    }              
                                                                                                                     
    printf("key: %u  shmid: %d\n", key, shmid);
    sleep(10);
    shmctl(shmid, IPC_RMID, NULL);//释放共享内存
    sleep(10);
    printf("key: 0x%x, shmid: %d -> shm delete success\n", key, shmid);
    return 0;
}

通过shell脚本查看共享内存的状态:

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

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

上文我们提到ipcs是查看进程间通信设施的信息的,这里的perms是共享内存的权限,此时为0,表示没有任何权限,所以我们在创建共享内存的时候,想要获得权限可以如下操作:

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

6、共享内存的关联(挂接)

将共享内存连接到进程地址空间需要用shmat函数,连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问;,shmat函数的函数原型如下:

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

参数说明:

  • shmid:是由shmget函数返回的共享内存标识。
  • shm_addr:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
  • shm_flg:是一组标志位,通常为0。SHM_RDONLY:为只读模式,其他为读写模式。

返回值:

  • shmat调用成功,返回共享内存映射到进程地址空间中的起始地址
  • shmat调用失败,返回(void*) -1
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
 
#define PATH_NAME "./" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096      //共享内存的大小
 
int main()    
{   
    key_t key = ftok(PATH_NAME, PROJ_ID); //获取key    
    if(key < 0){    
        perror("ftok");    
        return 1;    
    }    
 
    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建共享内存并设置权限   
    if(shmid < 0){    
        perror("shmget");    
        return 2;    
    }    
    printf("key: %u , shmid: %d\n", key, shmid);    
    sleep(10);    
    
    char* mem = (char*)shmat(shmid, NULL, 0);  //休眠10s后,关联共享内存                                                         
    printf("attaches shm success\n");    
    sleep(5);    
    
    shmdt(mem);    //5秒后,共享内存去关联
 
    printf("detaches shm success\n");    
    sleep(5);    
    
    shmctl(shmid, IPC_RMID, NULL);    //释放共享内存
    printf("key: 0x%x, shmid: %d -> shm delete success\n", key, shmid);    
    
    sleep(10);    
    return 0;    
}

7、共享内存去关联

取消共享内存与进程地址空间之间的关联需要用shmdt函数,与shmat函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存;(并不是释放共享内存),shmdt函数的函数原型如下:

#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);

参数说明:

  • shmaddr:连接的共享内存的起始地址,是shmat函数返回的地址指针。

返回值:

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

8、用共享内存实现server&client通信

刚刚我们是一个进程和共享内存关联的,接下来我们让两个进程通过共享内存进行通信;在线之前我们先测试一下这两个进程能否成功挂接到同一个共享内存上;

shmdata.h

#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
 
#define TEXT_SZ 2048
 
struct shared_use_st
{
	int written;//作为一个标志,非0:表示可读,0表示可写
	char text[TEXT_SZ];//记录写入和读取的文本
};
 
#endif

shmread.c

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include "shmdata.h"
 
int main()
{
	int running = 1;//程序是否继续运行的标志
	void *shm = NULL;//分配的共享内存的原始首地址
	struct shared_use_st *shared;//指向shm
	int shmid;//共享内存标识符
	key_t shm_key = ftok(FILE_PATH, 'a'));//获取系统IPC键值
	//创建共享内存
	shmid = shmget(shm_key, sizeof(struct shared_use_st), 0666|IPC_CREAT);
	if(shmid == -1)
	{
		fprintf(stderr, "shmget failed\n");
		exit(EXIT_FAILURE);
	}
	//将共享内存连接到当前进程的地址空间
	shm = shmat(shmid, 0, 0);
	if(shm == (void*)-1)
	{
		fprintf(stderr, "shmat failed\n");
		exit(EXIT_FAILURE);
	}
	printf("\nMemory attached at %X\n", (int)shm);
	//设置共享内存
	shared = (struct shared_use_st*)shm;
	shared->written = 0;
	while(running)//读取共享内存中的数据
	{
		//没有进程向共享内存定数据有数据可读取
		if(shared->written != 0)
		{
			printf("You wrote: %s", shared->text);
			sleep(rand() % 3);
			//读取完数据,设置written使共享内存段可写
			shared->written = 0;
			//输入了end,退出循环(程序)
			if(strncmp(shared->text, "end", 3) == 0)
				running = 0;
		}
		else//有其他进程在写数据,不能读取数据
			sleep(1);
	}
	//把共享内存从当前进程中分离
	if(shmdt(shm) == -1)
	{
		fprintf(stderr, "shmdt failed\n");
		exit(EXIT_FAILURE);
	}
	//删除共享内存
	if(shmctl(shmid, IPC_RMID, 0) == -1)
	{
		fprintf(stderr, "shmctl(IPC_RMID) failed\n");
		exit(EXIT_FAILURE);
	}
	exit(EXIT_SUCCESS);
}

shmwrite.c

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>
#include "shmdata.h"
 
int main()
{
	int running = 1;
	void *shm = NULL;
	struct shared_use_st *shared = NULL;
	char buffer[BUFSIZ + 1];//用于保存输入的文本
	int shmid;
	key_t shm_key = ftok(FILE_PATH, 'a'));//获取系统IPC键值
	//创建共享内存
	shmid = shmget(shm_key , sizeof(struct shared_use_st), 0666|IPC_CREAT);
	if(shmid == -1)
	{
		fprintf(stderr, "shmget failed\n");
		exit(EXIT_FAILURE);
	}
	//将共享内存连接到当前进程的地址空间
	shm = shmat(shmid, (void*)0, 0);
	if(shm == (void*)-1)
	{
		fprintf(stderr, "shmat failed\n");
		exit(EXIT_FAILURE);
	}
	printf("Memory attached at %X\n", (int)shm);
	//设置共享内存
	shared = (struct shared_use_st*)shm;
	while(running)//向共享内存中写数据
	{
		//数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本
		while(shared->written == 1)
		{
			sleep(1);
			printf("Waiting...\n");
		}
		//向共享内存中写入数据
		printf("Enter some text: ");
		fgets(buffer, BUFSIZ, stdin);
		strncpy(shared->text, buffer, TEXT_SZ);
		//写完数据,设置written使共享内存段可读
		shared->written = 1;
		//输入了end,退出循环(程序)
		if(strncmp(buffer, "end", 3) == 0)
			running = 0;
	}
	//把共享内存从当前进程中分离
	if(shmdt(shm) == -1)
	{
		fprintf(stderr, "shmdt failed\n");
		exit(EXIT_FAILURE);
	}
	sleep(2);
	exit(EXIT_SUCCESS);
}

9、共享内存的总结

共享内存:

  • 要使用一块共享内存,进程必须首先分配它。随后需要访问这个共享内存块的每一个进程都必须将这个共享内存绑定到自己的地址空间中。
  • 在 Linux 系统中,每个进程的虚拟内存是被分为许多页面的。这些内存页面中包含了实际的数据。每个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系。尽管每个进程都有自己的内存地址,不同的进程可以同时将同一个内存页面映射到自己的地址空间中,从而达到共享内存的目的。
  • 分配一个新的共享内存块会创建新的内存页面。因为所有进程都希望共享对同一块内存的访问,只应由一个进程创建一块新的共享内存。再次分配一块已经存在的内存块不会创建新的页面,而只是会返回一个标识该内存块的标识符。
  • 一个进程如需使用这个共享内存块,则首先需要将它绑定到自己的地址空间中。这样会创建一个从进程本身虚拟地址到共享页面的映射关系。当对共享内存的使用结束之后,这个映射关系将被删除。当再也没有进程需要使用这个共享内存块的时候,必须有一个(且只能是一个)进程负责释放这个被共享的内存页面。
  • 所有共享内存块的大小都必须是系统页面大小的整数倍。系统页面大小指的是系统中单个内存页面包含的字节数。在 Linux 系统中,内存页面大小是4KB,不过您仍然应该通过调用 getpagesize 获取这个值(通过man 2 getpagesize查看 )。
  • 共享内存的生命周期是随内核的,而管道是随进程的。
  • 共享内存不提供任何的同步和互斥机制,需要程序员自行保证数据安全。
  • 共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。

3、system V消息队列

1、消息队列的基本原理

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

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

总结:

  • 消息队列提供了一个从一个进程向另一个进程发送数据块的方法。
  • 每个数据块都被认为是有一个类型的,接收者进程接收的数据块可以有不同的类型值。
  • 和共享内存一样,消息队列的资源也必须自行删除,否则不会自动清除,因为system V IPC资源的生命周期是随内核的。
  • Linux用宏MSGMAX和MSGMNB来限制一条消息的最大长度和一个队列的最大长度。

2、消息队列数据结构

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

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

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

记录一下:

共享内存的数据结构msqid_dsipc_perm结构体分别在/usr/include/linux/msg.h和/usr/include/linux/ipc.h中定义。

3、消息队列的创建

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

int msgget(key_t key, int msgflg);

说明一下:

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

与其他的IPC机制一样,程序必须提供一个键来命名某个特定的消息队列。msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,而只返回一个标识符。

4、消息队列的释放

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

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

说明一下:

  • msgid:是由msgget函数返回的消息队列标识符。

  • ommand:是将要采取的动作,它可以取3个值,

    • IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
    • IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值
    • IPC_RMID:删除消息队列
  • buf:是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。msgid_ds结构至少包括以下成员:

    • struct msgid_ds
      {
          uid_t shm_perm.uid;
          uid_t shm_perm.gid;
          mode_t shm_perm.mode;
      };
      

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

5、向消息队列发送数据

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

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

msgsnd函数的参数说明:

  • msqid:表示消息队列的用户级标识符。
  • msgp:表示待发送的数据块。
  • msgsz:表示所发送数据块的大小,注意是消息的长度,而不是整个结构体的长度,也就是说msg_sz是不包括长整型消息类型成员变量的长度。
  • msgflg:表示发送数据块的方式,用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情,一般默认为0即可。

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

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

msgsnd函数的返回值说明:

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

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

6、从消息队列中获取数据

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

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

msgrcv函数的参数说明:

  • msqid:表示消息队列的用户级标识符。
  • msgp:表示获取到的数据块,是一个输出型参数。
  • msgsz:表示要获取数据块的大小。
  • msgtyp:表示要接收数据块的类型。可以实现一种简单的接收优先级。如果msgtype为0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息。
  • msgflg:用于控制当队列中没有相应类型的消息可以接收时将发生的事情。是控制函数行为的标志,取值可以是:
    • 0表示忽略;
    • IPC_NOWAIT,如果消息队列为空,则返回一个ENOMSG,并将控制权交回调用函数的进程。如果不指定这个参数,那么进程将被阻塞直到函数可以从队列中得到符合条件的消息为止。如果一个client 正在等待消息的时候队列被删除,EIDRM 就会被返回。如果进程在阻塞等待过程中收到了系统的中断信号,EINTR 就会被返回。
    • MSG_NOERROR,如果函数取得的消息长度大于msgsz,将只返回msgsz 长度的信息,剩下的部分被丢弃了。如果不指定这个参数,E2BIG 将被返回,而消息则留在队列中不被取出。

msgrcv函数的返回值说明:

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

7、例程

接收消息队列例程

接收信息的程序源文件为msgreceive.c的源代码为:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/msg.h>
 
struct msg_st
{
	long int msg_type;
	char text[BUFSIZ];
};
 
int main()
{
	int running = 1;
	int msgid = -1;
	struct msg_st data;
	long int msgtype = 0; //注意1
    key_t msg_key = Ftok(FILD_LOAD, 'a'));//获取系统IPC键值
	//建立消息队列
	msgid = msgget(msg_key, 0666 | IPC_CREAT);
	if(msgid == -1)
	{
		fprintf(stderr, "msgget failed with error: %d\n", errno);
		exit(EXIT_FAILURE);
	}
	//从队列中获取消息,直到遇到end消息为止
	while(running)
	{
		if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1)
		{
			fprintf(stderr, "msgrcv failed with errno: %d\n", errno);
			exit(EXIT_FAILURE);
		}
		printf("You wrote: %s\n",data.text);
		//遇到end结束
		if(strncmp(data.text, "end", 3) == 0)
			running = 0;
	}
	//删除消息队列
	if(msgctl(msgid, IPC_RMID, 0) == -1)
	{
		fprintf(stderr, "msgctl(IPC_RMID) failed\n");
		exit(EXIT_FAILURE);
	}
	exit(EXIT_SUCCESS);
}

发送消息队列例程

发送信息的程序的源文件msgsend.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()
{
	int running = 1;
	struct msg_st data;
	char buffer[BUFSIZ];
	int msgid = -1;
    key_t msg_key = Ftok(FILE_PATH, 'a'));//获取系统IPC键值
	//建立消息队列
	msgid = msgget(msg_key, 0666 | IPC_CREAT);
	if(msgid == -1)
	{
		fprintf(stderr, "msgget failed with error: %d\n", errno);
		exit(EXIT_FAILURE);
	}
 
	//向消息队列中写消息,直到写入end
	while(running)
	{
		//输入数据
		printf("Enter some text: ");
		fgets(buffer, BUFSIZ, stdin);
		data.msg_type = 1;    //
		strcpy(data.text, buffer);
		//向队列发送数据
		if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1)
		{
			fprintf(stderr, "msgsnd failed\n");
			exit(EXIT_FAILURE);
		}
		//输入end结束输入
		if(strncmp(buffer, "end", 3) == 0)
			running = 0;
		sleep(1);
	}
	exit(EXIT_SUCCESS);
}

4、system V信号量

信号量是一个计数器,与其它进程间通信方式不大相同,它主要用于控制多个进程间或一个进程内的多个线程间对共享资源的访问,相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志,除了用于共享资源的访问控制外,还可用于进程同步。
  它常作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源,因此,主要作为进程间以及同一个进程内不同线程之间的同步手段。Linux 提供了一组精心设计的信号量接口来对信号量进行操作,它们声明在头文件 sys/sem.h 中。

为了进行临界区保护。信号量一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。
  信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。
  由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
  P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
  V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.
  例如:两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

1、信号量相关概念

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

2、信号量数据结构

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

信号量的数据结构如下:

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

记录一下:

共享内存的数据结构msqid_dsipc_perm结构体分别在/usr/include/linux/sem.h和/usr/include/linux/ipc.h中定义。

3、信号量集的创建

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

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

参数说明:

  • key:信号量关联的键,由ftok函数产生。不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。
  • nsems:指定需要的信号量数目,它的值几乎总是1。
  • semget:是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。

返回值:

  • 相应信号标识符(非零),失败返回-1.

4、信号量集的删除

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

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

参数说明:

  • sem_id、sem_num两个参数与semget函数中的一样。

  • command:通常是下面两个值中的其中一个

    • SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
    • IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
  • 如果有第四个参数,它通常是一个union semum结构,定义如下:

    • union semun{
          int val;
          struct semid_ds *buf;
          unsigned short *arry;
      };
      

5、信号量集的操作

在 Linux 下,PV 操作通过调用semop函数来实现。它的作用是改变信号量的值,原型为:

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

参数说明:

  • sem_id:是由semget返回的信号量标识符

  • sembuf:结构的定义如下:

    • struct sembuf{
          short sem_num;//除非使用一组信号量,否则它为0
          short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
                          //一个是+1,即V(发送信号)操作。
          short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
                          //并在进程没有释放该信号量而终止时,操作系统释放信号量
      };
      
  • num_sem_ops:信号操作结构的数量,恒大于或等于1。

semop函数执行P操作时通常执行以下操作

 struct  sembuf  buf  = { 0, -1, SEM_UNDO};
 semop ( semid, &buf, 1) 

semop函数执行V操作时通常执行以下操作

 struct  sembuf  buf  = { 0, 1, SEM_UNDO};
 semop ( semid, &buf, 1) 

6、例程

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/sem.h>
 
union semun
{
	int val;
	struct semid_ds *buf;
	unsigned short *arry;
};
 
static int sem_id = 0;
static int set_semvalue()
{
	//用于初始化信号量,在使用信号量前必须这样做
	union semun sem_union;
 
	sem_union.val = 1;
	if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
		return 0;
	return 1;
}
 
static void del_semvalue()
{
	//删除信号量
	union semun sem_union;
 
	if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
		fprintf(stderr, "Failed to delete semaphore\n");
}
 
static int semaphore_p()
{
	//对信号量做减1操作,即等待P(sv)
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = -1;//P()
	sem_b.sem_flg = SEM_UNDO;
	if(semop(sem_id, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_p failed\n");
		return 0;
	}
	return 1;
}
 
static int semaphore_v()
{
	//这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = 1;//V()
	sem_b.sem_flg = SEM_UNDO;
	if(semop(sem_id, &sem_b, 1) == -1)
	{
		fprintf(stderr, "semaphore_v failed\n");
		return 0;
	}
	return 1;
}
int main(int argc, char *argv[])
{
	char message = 'X';
	int i = 0;
 	key_t sem_key = Ftok(FILE_PATH, 'a'));//获取系统IPC键值
	//创建信号量
	sem_id = semget(sem_key, 1, 0666 | IPC_CREAT);
 
	if(argc > 1)
	{
		//程序第一次被调用,初始化信号量
		if(!set_semvalue())
		{
			fprintf(stderr, "Failed to initialize semaphore\n");
			exit(EXIT_FAILURE);
		}
		//设置要输出到屏幕中的信息,即其参数的第一个字符
		message = argv[1][0];
		sleep(2);
	}
	for(i = 0; i < 10; ++i)
	{
		//进入临界区
		if(!semaphore_p())
			exit(EXIT_FAILURE);
		//向屏幕中输出数据
		printf("%c", message);
		//清理缓冲区,然后休眠随机时间
		fflush(stdout);
		sleep(rand() % 3);
		//离开临界区前再一次向屏幕输出数据
		printf("%c", message);
		fflush(stdout);
		//离开临界区,休眠随机时间后继续循环
		if(!semaphore_v())
			exit(EXIT_FAILURE);
		sleep(rand() % 2);
	}
 
	sleep(10);
	printf("\n%d - finished\n", getpid());
 
	if(argc > 1)
	{
		//如果程序是第一次被调用,则在退出前删除信号量
		sleep(3);
		del_semvalue();
	}
	exit(EXIT_SUCCESS);
}

7、进程互斥

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

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

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

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

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

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

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

8、system V IPC联系

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

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

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

转载说明:
1、https://blog.csdn.net/sjsjnsjnn/article/details/125864580?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~Rate-1-125864580-blog-121184624.235^v38^pc_relevant_anti_t3_base&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~Rate-1-125864580-blog-121184624.235^v38^pc_relevant_anti_t3_base&utm_relevant_index=1

2、https://blog.csdn.net/chenlong_cxy/article/details/121184624?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169232803216800186521421%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=169232803216800186521421&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2

3、https://blog.csdn.net/xxxx123041/article/details/127803054?spm=1001.2014.3001.5502

4、https://blog.csdn.net/xxxx123041/article/details/127812218?spm=1001.2014.3001.5502

5、https://blog.csdn.net/xxxx123041/article/details/127813086?spm=1001.2014.3001.5502

标签:信号量,include,int,间通信,key,linux,进程,共享内存
From: https://www.cnblogs.com/Rxin/p/17642255.html

相关文章

  • Linux命令
    常用命令命令ls-a这个选项能显示.开头的隐藏文件-i显示每个文件的inode号-m所有项目以逗号分隔,并填满整行行宽-R同时列出所有子目录层-h将列出文件的大小以人性化格式输出ls-lc[文件名]查看文件的访问时间ls-lu[文件名]查看文件的最后修改时间ls-l显示文件的......
  • linux下gcc/g++创建一个共享库项目以及创建一个可执行项目动态链接该共享库
    1.先确保有g++命令2.创建一个c++项目目录,并cd到该目录3.创建共享库头文件:dynamic_so.h#ifndef__TEST__#define__TEST__inttestFun(inta,intb);#endif4.创建对应共享库的实现文件:dynamic_so.cpp#include"dynamic_so.h"inttestFun(inta,intb){returna......
  • linux环境下基于python的OpenCV 保存视频
    一概念在OpenCV中保存视频使用的是VedioWriter对象,在其中指定输出文件的名称,A创建视频写入的对象out=cv2.VideoWriter(filename,fourcc,fps,frameSize)参数含义:filename:视频保存的位置fourcc:指定视频编解码器的4字节代码fps:帧率frameSize:帧大小B 设置视频的编解......
  • 基于 Debian 12 的MX Linux 23 正式发布!
    MX Linux 是基于Debian稳定分支的面向桌面的Linux发行,它是antiX及早先的MEPISLinux社区合作的产物。它采用Xfce作为默认桌面环境,是一份中量级操作系统,并被设计为优雅而高效的桌面与如下特性的结合:配置简单、高度稳定、性能可靠、占用空间不大也不小。新版本......
  • Hadoop3.3.0--Linux编译安装
    Hadoop3.3.0--Linux编译安装本实验内容教程来源于“黑马程序员”如有侵权请联系作者删除基础环境:Centos7.7编译环境软件安装目录mkdir-p/export/server一、Hadoop编译安装(选做)可以直接使用课程提供已经编译好的安装包。安装编译相关的依赖yuminstallgccgcc-c+......
  • Linux unixODBC 连接 MySQL数据库
    LinuxunixODBC连接MySQL数据库1.下载unixODBC及MySQLConnector/ODBC1.1unixODBC下载https://www.unixodbc.org/ 1.2MySQLConnector/ODBC下载https://downloads.mysql.com/archives/c-odbc/选择版本选择:5.3.132. 安装驱动安装unixODBCtar-zxvfunix......
  • linux虚拟机中各服务端口及配置文件路径
    查询端口状况命令:netstat-an|grep端口号查询服务状态(服务是否开启)命令:systemctl  status服务名开启服务命令:systemctl  start  服务名21端口:FTP文件传输服务22端口:SSH协议、SCP(文件传输)、端口号重定向23/tcp端口:TELNET终端仿真服务25端口:SMTP简单邮件传输服务53......
  • Linux基础35 搭建博客,搭建知乎,搭建edusoho
    5.搭建博客wordpress1)上传包从https://cn.wordpress.org/下载wordpress-5.0.3-zh_CN.tar.gz[root@web01~]#cd/code/[root@web01code]#rzwordpress-5.0.3-zh_CN.tar.gz2)解压代码包[root@web01code]#tarxfwordpress-5.0.3-zh_CN.tar.gz3)授权目录[root......
  • 父/子进程文件描述符继承机制导致socket bind失败的问题
    此问题来自项目上,应用程序本身由它的父进程启动,父进程监听SIGCHLD信号,即子进程退出时,父进程会收到这个信号,然后立即通过execlp重新启动子进程,确保子进程异常崩溃会被重新拉起来。而子进程(我们实际的业务应用)也会在某些地方fork新的进程,干别的事情。出现的问题是,进程被重新拉起......
  • 【Python-每日技巧】python在linux中通过进程名称停止指定进程
    在CentOS下,你可以使用psutil库来杀死进程。以下是一个示例代码:importpsutil#设置要杀死的进程的名称process_name="your_process_name"#查找指定名称的进程并杀死forprocinpsutil.process_iter(['pid','name']):ifproc.info['name']==process_name:......