Linux-操作系统原理基础知识(二)

进程间通信(ipc)

进程间通信主要功能:实现不同进程间的数据传输、资源共享、事件通知、进程控制。

linux系统的进程间通信主要包括了四部分:(项目开发常用posix)

一、早期unix系统进程间通信:

  • 管道  主要完成数据传输功能
  • 信号  主要完成事件通知
  • fifo  主要完成数据传输;

二、system-v 进程间通信

system-v是unix系统的主分支,是贝尔实验室在早期unix上优化得到,在早期unix  ipc基础上额外新增加了以下三种ipc:

  • system-v  消息队列   不仅可以完成数据传输功能,还能起到控制进程的功能。
  • system-v  信号量      主要完成资源共享和进程控制功能
  • system-v  共享内存  主要功能是完成数据传输【高效】;

三、socket ipc(是由unix系统的另一个分支BSD系统开发出来的。)

主要用在允许不同机器间进程通信;

四、posix ipc(IEEE创建的ipc)

统一unix系统下面的ipc的操作接口。

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

管道

        当数据从一个进程连接流到另一个进程时,这之间的连接就是一个管道(pipe)。我们通常是把一个进程的输出通过管道连接到另一个进程的输入。
        对于 shell 命令来说,命令的连接是通过管道字符来完成的,正如“ps -aux | grep root ”命令一样,使用“|”字符进行连接即可。比如:在ps -aux | grep root命令中,ps 命令是列出当前的进程, grep 命令也使用过,是一种文本搜索工具,它能使用正则表达式搜索文本。

ps与 grep命令之间的“|”符号其实就是一个管道,将 ps 命令输出的数据通过管道流向 grep,其实在这里就打开了两个进程, ps 命令本应该在终端输出信息的,但是它通过管道将输出的信息作为 grep 命令的输入信息,然后通过搜索之后将合适的信息显示出来,这样子就形成了我们在终端看到的信息。

如下图:验证是否打开了两个进程,使用ps -ux | grep $USER命令查看当前的进程情况,在输出的最后打印两个进程相关的信息(ps、 grep):

对” ps -aux | grep root”命令进行详细的分析,实际上是执行以下过程:

  • shell 负责安排两个命令的标准输入和标准输出。
  • ps 的标准输入来自终端鼠标、键盘等。
  • ps 的标准输出传递给 grep,作为 grep 的标准输入。
  • grep 的标准输出连接到终端,即输出到显示器屏幕,最终我们看到 grep 的输出结果。

shell 所做的工作实际上是对标准输入和标准输出流进行了重新连接,在 ps 命令与 grep 之间建立了数据管道,示意图如下:

        管道本质上也是一个文件,上图的过程可以看作是 ps 进程将输出的内容写入管道中, grep进程从管道中读取数据,可以把它抽象成一个可读写的文件。遵循了 Linux 中“一切皆文件”的设计思想,它借助 VFS(虚拟文件系统)给应用程序提供操作接口,实现了管道的功能。

        虽然管道的实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间,它占用的是内存空间,因此 Linux 上的管道就是一个操作方式为文件的内存缓冲区。

Linux 系统上的管道分两种类型:匿名管道和命名管道。这两种管道也叫做无名或有名管道。匿名管道最常见的形态就是我们在 shell 操作中最常用的 “|”。它的特点是只能在父子进程中使用,父进程在产生子进程前必须打开一个管道文件,然后 fork 产生子进程,这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符,以达到使用同一个管道通信的目的。此时除了父子进程外,没人知道这个管道文件的描述符,所以通过这个管道中的信息无法传递给其他进程。这保证了传输数据的安全性,但降低了管道了通用性,于是系统提供了命名管道,它本质是一个文件,位于文件系统中,命名管道可以让多个无相关的进程进行通讯。

匿名管道 PIPE

匿名管道(PIPE)是一种特殊的文件,但虽然它是一种文件,却没有名字,因此一般进程无法使用 open() 来获取他的描述符。无名管道只能在一个进程中被创建出来,然后通过继承的方式将他的文件描述符传递给子进程,这就是为什么匿名管道只能用于亲缘关系进程间通信的原因。

匿名管道特点

匿名管道不同于一般文件的显著之处是:它有两个文件描述符,而不是一个,一个只能用来读,另一个只能用来写,这就是所谓的“半双工”通信方式。而且它对写操作不做任何保护,即:假如有多个进程或线程同时对匿名管道进行写操作,那么这些数据很有可能会相互践踏,因此:匿名管道只能用于一对一的亲缘进程通信。匿名管道不能使用 lseek() 来进行所谓的定位,因为他们的数据不像普通文件那样按块的方式存放在诸如硬盘、 flash 等块设备上。

总结来说,匿名管道有以下的特征:

  • 没有名字,因此不能使用 open() 函数打开,但可以使用 close() 函数关闭。
  • 只提供单向通信(半双工),也就是说,两个进程都能访问这个文件,假设进程 1 往文件内写东西,那么进程 2 就只能读取文件的内容。
  • 只能用于具有血缘关系的进程间通信,通常用于父子进程建通信。
  • 管道是基于字节流来通信的。
  • 依赖于文件系统,它的生命周期随进程的结束而结束。
  • 写入操作不具有原子性,因此只能用于一对一的简单通信情形。
  • 管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read() 和 write() 等函数,但是它又不是普通的文件,并不属于其他任何文件系统,并且只存在于内核的内存空间中,因此不能使用 lseek() 来定位。
  • 当匿名管道所对应的所有的文件描述符都被关闭之后,匿名管道自动被销毁。(父子进程各自的两个一共四个文件描述符被关闭,匿名管道自动被销毁。)

对匿名管道进行read() 和 write()操作可能会阻塞当前的进程,有两种情况:

第一种情况,使用read()函数读取匿名管道内容时候,里面没有内容就会阻塞当前进程,直到匿名管道再被写入数据内容之后,read()函数才会继续执行。

第二种情况,使用write()函数往匿名管道写入数据的时候,当writ把匿名管道的缓存区全部写满的时候,write函数所对应的进程就不能继续再往匿名管道写入数据了,就阻塞在此,直到匿名管道的数据被读走,write函数才能继续运行往无名管道写入数据。

匿名管道使用步骤

  • 父进程pipe 匿名管道:父进程先创建匿名管道。
  • fork 子进程:子进程将父进程里的文件描述符表一并继承,这样父子进程都可对匿名管道进行操作。
  • close 无用端口:父子进程间传输数据一般是单方面传输数据:要么是父传给子要么是子传父,这种情况下:在父进程里面只是用到他的读的端口或者写的端口,一般来说不会同时使用到读写端口,此时就用close函数关闭没有用到的另一个端口即可。
  • write/read 读/写匿名管道端口
  • close 关闭匿名管道的读/写端口,无名管道就会被自动销毁了

pipe()函数

pipe() 函数用于创建一个匿名管道,一个可用于进程间通信的单向数据通道。调用了pipe函数之后,pipe函数会在Linux内核里面创建一个匿名管道,并且把此匿名管道的文件描述符记录在pipefd[2]这个数组里面。在父子进程里面就可以通过 pipefd 数组里面记录的文件描述符来对无名管道进行读写操作。

数组 pipefd 是用于返回两个引用管道末端的文件描述符,它是一个由两个文件描述符组成的数组的指针。 pipefd[0] 指管道的读取端, pipefd[1]指向管道的写端,向管道的写入端写入数据将会由内核缓冲,即写入内存中,直到从管道的读取端读取数据为止,而且数据遵循先进先出原则。

头文件:#include <unistd.h>
函数原型:int pipe(int pipefd[2]);
参数:pipefd[2]数组是一个文件描述符数组,主要记录了匿名管道的读的和写的文件描述符
返回值:成功返回0;失败返回-1

想要父子进程间有数据交互,则需要以下操作:

  • 父进程调用 pipe() 函数创建匿名管道,得到两个文件描述符 pipefd[0]、 pipefd[1],分别指向管道的读取端和写入端。
  • 父进程调用 fork() 函数启动(创建)一个子进程,那么子进程将从父进程中继承这两个文件描述符 pipefd[0]、 pipefd[1],它们指向同一匿名管道的读取端与写入端。
  • 由于匿名管道是利用环形队列实现的,数据将从写入端流入管道,从读取端流出,这样子就实现了进程间通信,但是这个匿名管道此时有两个读取端与两个写入端,如图 fork 后子进程继承父进程文件描述符 所示,因此需要进行接下来的操作。
  • 如果想要从父进程将数据传递给子进程,则父进程需要关闭读取端,子进程关闭写入端,数据从父进程流向子进程。
  • 如果想要从子进程将数据传递给父进程,则父进程需要关闭写入端,子进程关闭读取端,数据从子进程流向父进程。
  • 当不需要管道的时候,就在进程中将未关闭的一端关闭即可。

如下代码实习上述父子进程间的数据交互:

//父进程写、子进程读
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define MAX_DATA_LEN 256
int main()
{
	pid_t pid;
	int pipe_fd[2];				//0--读描述符	1--写描述符
	int status;
	char buf[MAX_DATA_LEN];
	const char data[]="Pipe test program";
	int real_read_num,real_write_num;
 
	memset( (void *)buf,0,sizeof(buf) );
 
	//创建管道
	if( pipe(pipe_fd)<0 )			//创建管道失败
	{
		printf("pipe create error\n");
		exit(1);
	}
	//创建子进程
	if( (pid=fork()) == 0 )			//创建管道成功则创建子进程
	{
		//子进程关闭写描述符
		close(pipe_fd[1]);
		//子进程读取管道内容【无数据则阻塞】
		if(  ( real_read_num = read(pipe_fd[0],buf,MAX_DATA_LEN) ) > 0  )
			printf("%d bytes read form the pipe is '%s'\n",real_read_num,buf);
	
		//关闭子进程读描述符
		close(pipe_fd[0]);
		exit(0);
	}
	else if(pid>0)
	{
		//父进程关闭读描述符
		close(pipe_fd[0]);
		//父进程写数据【缓冲区满则阻塞】
		if(    ( real_write_num = write(pipe_fd[1],data,strlen(data)) ) != -1   )
			printf("Parent write %d bytes : '%s'\n",real_write_num,data);
	
		//关闭父进程读描述符
		close(pipe_fd[1]);
		//收集子进程退出信息
		wait(&status);
		exit(0);
	}
}

命名管道 FIFO

命名管道特点

命名管道(FIFO)与匿名管道不同,命名管道可以在多个无关的进程中交换数据(通信)。

匿名管道的通信方式通常都由一个共同的祖先进程启动,只能在”有血缘关系”的进程中交互数据,这给我们在不相关的的进程之间交换数据带来了不方便,因此产生了命名管道,来解决不相关进程间的通信问题。命名管道不同于无名管道之处在于它提供了一个路径名与之关联,以一个文件形式存在于文件系统中,这样,即使与命名管道的创建进程不存在“血缘关系”的进程,只要可以访问该命名管道文件的路径,就能够彼此通过命名管道相互通信,因为可以通过文件的形式,那么就可以调用系统中对文件的操作,如打开(open)、读(read)、写(write)、关闭(close)等函数,虽然命名管道文件存储在文件系统中,但数据却是存在于内存中的,这点要区分开。

总结来说,命名管道有以下的特征:

  • 有名字,存储于普通文件系统之中。
  • 任何具有相应权限的进程都可以使用 open() 来获取命名管道的文件描述符。
  • 跟普通文件一样:使用统一的 read() / write() 来读写。
  • 跟普通文件不同:不能使用 lseek() 来定位,原因是数据存储于内存中。
  • write具有原子性,支持多写者同时进行写操作而数据不会互相践踏。
  • 遵循先进先出(FIFO)原则,最先被写入 FIFO 的数据,最先被读出来。
  • write和read操作可能会阻塞进程。

mkfifo()函数

头文件:#include <sys/types.h>    #include <sys/state.h>
函数原型:int mkfifo(const char *filename,mode_t mode);
char *filename:命名管道所对应的文件的文件名
mode_t mode:代表命名管道文件的权限(只读,只写,可读写)
返回值:成功返回0;失败返回-1

使用步骤

  • 第一个进程使用mkfifo创建一个命名管道。
  • open()函数就可打开命名管道,write/read函数进行数据的写/读。
  • close()函数关闭命名管道。
  • 第二个进程open()打开命名管道,read/write读/写数据。
  • close()关闭命名管道。

代码

此进程是循环读取管道的数据:

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/wait.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//有名管道文件名
#define MYFIFO "/tmp/myfifo"
//4096 定义在 limits.h 中
#define MAX_BUFFER_SIZE PIPE_BUF
 
int main(int argc,char *argv[])//参数为即将写入的字符串
{
	char buff[MAX_BUFFER_SIZE];
	int fd;
	int nread;
	//判断有名管道是否已存在,若尚未创建,则以相应的权限(666可读可写)创建
	if(access(MYFIFO,F_OK) == -1)	//MYFIFO目录下命名管道存在,access宏等于0,不存在等于-1
	{
		printf("Fifo doesn't exist\n");			//fifo不存在
		if( (mkfifo(MYFIFO,0666)<0) && (errno!=EEXIST)  )
		{
			printf("Can't create fifo file\n");	//创建fifo失败
			exit(1);
		}
		printf("Create fifo success\n");		//创建fifo成功
	}
	else
		printf("Fifo exist\n");				//fifo存在
 
	fd = open(MYFIFO,O_RDONLY);	    //O_RDONLY这个宏表示以只读阻塞方式打开有名管道	
	if(fd==-1)
	{
		printf("Open fifo file error");			//打开fifo失败	
		exit(1);
	}
	printf("Open fifo file success\n");			//打开fifo成功	
	//循环读取有名管道数据
	while(1)
	{
		memset(buff,0,sizeof(buff));
		if( (nread = read(fd,buff,MAX_BUFFER_SIZE)) > 0  )
			printf("Read '%s' from fifo\n",buff);
	}
}

此进程是往命名管道写入数据:

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/wait.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
//有名管道文件名
#define MYFIFO "/tmp/myfifo"
//4096 定义在 limits.h 中
#define MAX_BUFFER_SIZE PIPE_BUF
int main(int argc,char *argv[])//参数为即将写入的字符串
{
	char buff[MAX_BUFFER_SIZE];
	int fd;
	int nwrite;
	if(argc<=1)
	{
		printf("Usage: ./fifo_write string\nPara error\n");
		exit(1);
	}
	//填充命令行第一个参数到buff
	sscanf(argv[1],"%s",buff);
 
	//以只写阻塞方式打开有名管道
	printf("Ready to open fifo\n");
	fd = open(MYFIFO,O_WRONLY);				//若管道未创建,则阻塞在这里	
	
	if(fd==-1)
	{
		printf("Open fifo file error\n");		//打开fifo失败	
		exit(1);
	}
	printf("Open fifo file success\n");
 
	//向管道中写入字符串
	if( (nwrite=write(fd,buff,MAX_BUFFER_SIZE)) > 0 )	
		printf("Write '%s' to fifo\n",buff);
 
	close(fd);
	exit(0);
}

信号

基本概念

信号(signal),又称软中断信号,用于通知进程发生了异步事件,它是 Linux 系统响应某些条件而产生的一个事件,它是在软件层次上对中断机制的一种模拟,是一种异步通信方式,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。
信号是进程间通信机制中唯一的异步通信机制,一个进程不必等待信号的到达,进程也不知道信号到底什么时候到达。类似中断服务函数一样,在中断发生的时候,就会进入中断服务函数中去处理。同样,当进程接收到一个信号时,也会相应地采取一些行动。可以使用术语“生成(raise)”表示一个信号的产生,使用术语“捕获(catch)”表示进程接收到一个信号。
在 Linux 系统中,信号可能由于系统中某些错误而产生,也可以是某个进程主动生成的一个信号。由于某些错误条件而生成的信号:如内存段冲突、浮点处理器错误或非法指令等,它们由shell 和终端处理器生成并且引起中断。由进程主动生成的信号可以作为在进程间传递通知或修改行为的一种方式,它可以明确地由一个进程发送给另一个进程,当进程捕获了这个信号就会按照程序进行相应并且去处理它。无论何种情况,它们的编程接口都是相同的,信号可以被生成、捕获、响应或忽略。进程之间可以互相发送信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。

Linux系统信号类型

通过:kill -l 命令查看,有64种信号类型,每种信号名称都以 SIG 三个字符开头

可将这 64 中信号分为 2 大类:信号值为 1~31 的信号属性非实时信号(也称不可靠信号),它们是Linux系统从早期 UNIX 系统中继承下来的信号,信号值为 34~64 的信号为实时信号(也被称为可靠信号),它们是后期Linux系统自己扩增的。

常用信号分析

一般而言,信号的响应处理过程如下:

  1. 如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。
  2. 如果该信号被捕获,那么进一步判断捕获的类型。
  3. 如果设置了响应函数,那么执行该响应函数。
  4. 如果设置为忽略,那么直接丢弃该信号。
  5. 最后才执行信号的默认处理方式。

pkill命令,杀死进程不需要指定进程的pid,只需指定进程的名称即可。(pkill  进程名称) 

信号名信号编号                                产生原因默认处理方式
SIGHUB1关闭终端终止
SIGINT2Ctrl+c终止
SIGQUIT3Ctrl+\终止+转储
SIGILL4CPU 检测到某进程执行了非法指令时产生
SIGABRT6abort(),进程中调用的话就是进程自己杀死自己终止+转储
SIGFPE8算术错误,比如:整数除0的非法操作终止
SIGKILL9kill -9 pid,往pdi指定进程发送一个9的信号,强制杀死终止,不可捕获/忽略
SIGUSR110自定义,可自定产生原因,自定处理方式忽略
SIGSEGV11段错误,一般是在进程里访问非法内存时产生终止+转储
SIGUSR212自定义,可自定产生原因,自定处理方式忽略
SIGPIPE13通常在进程间通信产生
SIGALRM14alarm(),设置一个定时,定时到之后发送一个终止信号终止
SIGTERM15控制台输入kill pid此命令所产生的终止
SIGCHLD17(子进程)发送状态变化所产生的忽略
SIGSTOP19Ctrl+z暂停,不可捕获/忽略

信号的产生

生成信号的事件一般可以归为 3 大类:程序错误、外部事件以及显式请求。

  • 程序错误如:零作除数、非法存储访问等,这种情况通常是由硬件而不是由 Linux 内核检测到的,但由内核向发生此错误的那个进程发送相应的信号。
  • 外部事件如:当用户在终端按下某些键时产生终端生成的信号,当进程超越了 CPU 或文件大小的限制时,内核会生成一个信号通知进程。
  • 显式请求如:使用 kill() 函数允许进程发送任何信号给其他进程或进程组。

软件层面产生信号

  • 驱动程序给应用程序发信号,一般是通知一些硬件方面的一些变化的。
  • 控制台通过命令产生信号:Ctrl+c:中断信号;Ctrl+ |(‘与符号’):退出信号;Ctrl+z:停止信号。
  • kill  -9命令强制杀死进程,一般杀死停止态进程。

信号的生成既可以是同步的,也可以是异步的。

  • 同步信号大多数是程序执行过程中出现了某个错误而产生的,由进程显式请求生成的给自己的信号也是同步的。
  • 异步信号是接收进程可控制之外的事件所生成的信号,这类信号一般是进程无法控制的,只能被动接收,因为进程也不知道这个信号会何时发生,只能在发生的时候去处理它。一般外部事件总是异步地生成信号,异步信号可在进程运行中的任意时刻产生,进程无法预期信号到达的时刻,它所能做的只是告诉 Linux 内核假如有信号生成时应当采取什么行动(这相当于注册信号对应的处理)。

信号的处理方式

当信号发生时,我们可以告诉 Linux 内核采取如下 3 种动作中的任意一种:

  • 忽略信号:进程当信号从来没发生过。大部分信号都可以被忽略,但有两个除外: SIGSTOP 和 SIGKILL 绝不会被忽略。不能忽略这两个信号的原因是为了给sudo用户提供杀掉或停止任何进程的一种手段。此外,尽管其他信号都可以被忽略,但其中有一些却不宜忽略。例如,若忽略硬件例外(非法指令)信号,则会导致进程的行为不确定。
  • 捕获信号:这种处理是要告诉 Linux 内核,当信号出现时调用专门提供的一个函数。这个函数称为信号处理函数,它专门对产生信号的事件作出处理。
  • 默认:让信号默认动作起作用。系统为每种信号规定了一个默认动作,这个动作由 Linux 内核来完成,有以下几种可能的默认动作:
  1. 终止进程并且生成内存转储文件,即写出进程的地址空间内容和寄存器上下文至进程当前目录下名为 cone 的文件中;
  2. 终止终止进程但不生成 core 文件。
  3. 忽略信号。
  4. 暂停进程。
  5. 若进程为暂停状态,恢复进程,否则将忽略信号

处理信号相关API函数

signal函数

signal() 主要是用于捕获信号,可以改变进程中对信号的默认行为,我们在捕获这个信号后,也可以自定义对信号的处理方式,当收到这个信号后,应该如何去处理它,这也是在开发 Linux最常使用的方式。使用 signal() 时,它需要提前设置一个回调函数,即进程接收到信号后将要跳转执行的响应函数,或者设置忽略某个信号,才能改变信号的默认行为,这个过程称为“信号的捕获”。对一个信号的“捕获”可以重复进行,不过 signal() 函数将会返回前一次设置的信号响应函数指针。

头文件:#include <signal.h>

函数原型:typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数:signum:要设置的信号
handler可设置的三种信号处理方式:SIG_IGN:忽略 SIG_DFL:默认  void(*sighandler_t)(int):自定义

返回值:成功返回上一次设置的handler;失败返回SIG_ERR

signal 是一个带有 signum 和 handler 两个参数的函数。

准备捕获或忽略的信号由参数 signum 指出

接收到指定的信号后将要调用的函数由参数 handler 指出。

handler 是一个函数指针,它的类型是 void(*sighandler_t)(int) 类型,拥有一个 int 类型的参数,这个参数的作用就是传递收到的信号值,返回类型为 void。(比如:当我们设置参数为自定义的时候,此函数指针void(*sighandler_t)(int) 就会指向我们自定义的一个函数。)

signal函数使用(代码演示):

//按下“CTRL+C”时,进入 signal_handler() 信号处理函数,打印对应的信息,由于设置为处理后恢复默认,因此当下一次按下“CTRL+C”时进程将直接退出
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
 
//信号处理函数(自定义函数)
void signal_handler(int sig)
{
	printf("\nThe signal number is %d\n",sig);
	if(sig==SIGINT)
	{
		printf("I have get SIGINT\n");
		printf("The signal has been restored to the default processing mode\n\n");

		//恢复信号为默认情况
		signal(SIGINT,SIG_DFL);
	}
}
int main(int argc,char *argv[])
{
	printf("\nThis is an signal test function\n\n");
	//设置信号处理的回调函数,设置信号处理方式为自定义函数方式
	signal(SIGINT,signal_handler);
	while(1)
	{
		printf("Waitinig for the SIGINT signal , please enter \"ctrl+c\" ...\n");
		sleep(1);
	}
	exit(0);
}

上面实验中我们通过“Ctrl+C”来发送了信号,在代码里,还可以通过调用 kill()、 raise()、 alarm()等信号发送函数。

kill函数

头文件:#include <signal.h>    #include <sys/types.h>
函数原型:int kill(pid_t pid,int sig);
参数:pid即进程id,sig:要发送的信号。
返回子:成功返回0,失败返回:-1

如果想发送一个信号给进程,而该进程并不是当前的前台进程,就需要使用 kill 命令。该命令需要有一个可选的信号代码或信号名称和一个接收信号的目标进程的 PID(这个 PID 一般需要用 ps 命令查出来)。例如:如果要向运行在另一个终端上 PID 为 666 的进程发送“挂断”信号(SIGHUP)就可使用如下命令:kill - SIGHUP 666或者 kill -1 666。而kill() 函数与 kill 系统命令一样,可以发送信号给进程或进程组,实际 kill 系统命令只是 kill() 函数的一个用户接口。kill()函数它不仅可以中止进程(实际上发出 SIGKILL 信号),也可以向进程发送其他信号。进程可以通过调用 kill() 函数向包括它本身在内的其他进程发送一个信号。如果程序没有发送该信号的权限,对 kill() 函数的调用就将失败,失败的常见原因是目标进程由另一个用户所拥有。因此要想发送一个信号,发送进程必须拥有相应的权限,这通常意味着两个进程必须拥有相同的用户 ID(即你只能发送信号给属于自己的进程,但超级用户可以发送信号给任何进程)。

raise函数

raise() 函数也是发送信号函数,不过与 kill() 函数所不同的是, raise() 函数只是进程向自身发送信号的,而没有向其他进程发送信号。raise()函数发送失败的原因主要是信号无效,因为它只往自身发送信号,不存在权限问题,也不存在目标进程不存在的情况

头文件:#include <signal.h>
函数原型:int raise(int sig);
参数:只有一个参数 sig,它代表着发送的信号值
返回值:发送成功返回 0,发送失败返回-1

如下实验代码,包含了 raise 与 kill 的使用示例:

#include <signal.h>
 
int main(void)
{
        pid_t pid;
        int ret;
        //创建子进程
        if( (pid=fork()) < 0 ){      //进程创建失败
                printf("Fork error\n");
                exit(1);
        }
        if(pid==0)                              //子进程                        
        {
                //在子进程中使用raise函数发出SIGSTOP信号,使子进程暂停
                printf("Child is waiting for SIGSTOP signal\n\n");
               
                raise(SIGSTOP);  //子进程停在这里【自己给自己发了停止信号】
                
                printf("Child won't run here forever\n");   //子进程不会执行到这里
 
                exit(0);
        }
        else                                    //父进程
        {
                //睡眠3秒,让子进程先执行
                sleep(3);
 
                //发送SIGKILL信号杀死子进程
                if( (ret=kill(pid,SIGKILL)) == 0 )
                        printf("Parent kill child which ID = %d\n\n",pid);
 
                //一直阻塞直到子进程被杀死
                wait(NULL);
 
                //父进程退出运行
                printf("Parent exit\n");
 
                exit(0);
        }
}

信号集处理函数

屏蔽信号集

屏蔽信号集作用:屏蔽某些具体信号。它实质上是一个64位的位图,Linux中的64种信号和64位的位图一一对应。当屏蔽信号集位图中对应位被置1之后,对应位所对应的信号就会被屏蔽。进程捕捉到信号之后,先去屏蔽信号集查询是否属于屏蔽信号,若不属于屏蔽信号才会对捕捉到的信号进行处理。若属于屏蔽信号则此信号进入未处理信号集。

设置屏蔽信号集

  • 手动:自己手动调用信号集相关API函数来设置屏蔽信号集。
  • 自动:进程在某些场景下会自动设置屏蔽信号集。比如进程正在处理某个信号,此信号的处理函数比较长,进程会捕捉到很多相同的信号,此时进程会自动把信号屏蔽。

当前信号处理完之后,进程才会到未处理信号集里面把之前屏蔽的信号提出来进行处理。

未处理信号集

被屏蔽的信号发生时进入未处理信号集

非实时信号(1-31信号类型),只保留1个:当把非实时信号挂载在未处理信号集上面的时候,挂载的多个非实时信号是不排队保留的,只保留1个。当前信号处理完之后,就会把挂载的1个非实时信号提取出来进行处理。

实时信号(34-64信号类型),按照 FIFO 进行排队:未处理信号集会保留全部的实时信号。

//运行后打印字符,按一次ctrl+c打印一次
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
 
void my_func(int signo)
{
        printf("\nhello\n");
        sleep(5);    //在sleep期间按下Ctrl+c所传入的SIGINT信号将会进入未处理信号集
        printf("world\n");
}
int main()
{
        signal(SIGINT,my_func);
        while(1);
        return 0;
}
信号集相关API函数

sigset_t定义一个sigset_t 信号集合类型的变量*set,把sigset_t *set变量的指针传给函数。

int sigemptyset(sigset_t *set);将信号集合初始化为0

int sigfillset(sigset_t *set);将信号集合初始化为1

int sigaddset(sigset_t *set,int signum);将信号集合某一位设置为1

int sigdelset(sigset_t *set,int signum);将信号集合某一位设置为0

上面四个都是来设置我们自己定义的信号集合的,我们自定的信号集合设置好之后,可以用下面这个函数去设置给信号屏蔽集。

int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);使用设置好的信号集去修改信号屏蔽集
参数how:用来设置我们设置屏蔽信号集时的方式。

SIG_BLOCK:屏蔽某个信号 (屏蔽信号集就变成了:屏蔽集自己 | 我们自己设置的set集合)
SIG_UNBLOCK:打开某个信号,解除对某个信号的屏蔽 (屏蔽集 & (~set))
SIGSETMASK:屏蔽集直接 = set也就是我们设置的信号集合。

参数sigset_t *set:就是我们自己设置的信号集合

参数oldset:保存旧的屏蔽集的值,NULL表示不保存之前的屏蔽信号集的值

解除自动屏蔽SIGINT信号,三次按下Ctrl+c,3个world几乎同时打印,类似3个进程几乎同时执行。

//运行后打印字符,按一次ctrl+c打印一次
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
 
void my_func(int signo)
{
	//解除自动屏蔽SIGINT信号
	sigset_t set;
	sigemptyset(&set);
	sigaddset(&set,SIGINT);
	sigprocmask(SIG_UNBLOCK,&set,NULL);
 
	printf("\nhello\n");
	sleep(5);
	printf("world\n");
}
 
int main()
{
	signal(SIGINT,my_func);
	//signal(35,my_func);
	while(1);
	return 0;
}

system-V IPC

system-V IPC特点

  • 独立于进程:无论在Linux系统里新增一个或者删除一个进程都不会影响到系统里面已经存在的system-V ipc。
  • 没有文件名和文件描述符:独立于进程的话肯定就不是利用文件名和文件描述符对其进行标识和控制的。(因为文件描述符记录在进程里面的,一旦进程被销毁,文件描述符也会废弃,这样就没办法通过文件描述符来控制进程间通信对象了)
  • ipc对象有key和ID:与文件名和文件描述符相类似的机制。key是用来唯一标识ipc的,我们通过ipc的ID对其进行控制的。

Systme-V 消息队列

Systme-V 消息队列用法

  • 定义一个唯一key(一般使用ftok()函数来生成一个唯一的键值)
ftok函数:用来获取一个key
函数原型:key_t ftok(const char *path,int proj_id)
参数:path:一个合法路径;    proj_id:一个整数
返回值:成功返回合法键值,失败返回-1
  • 构造消息对象(通过msgget()函数来实现)
msgget函数:获取消息队列ID        类似文件操作的open函数
函数原型:int msgget(key_t key,int msgflg);
参数:key:消息队列的键值
megflg两个选项:IPC_CREAT:如果消息队列不存在则创建,创建的时候设置信号量的mode:访问权限
返回值:成功返回该消息队列的ID,失败返回-1
  • 发送特定类型消息(通过msgsnd()函数来实现)
msgsnd函数:发送消息到消息队列
函数原型:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:msqid:消息队列的id   
*msgp:指向消息缓存区的指针,如下消息缓存区的经典结构:
struct msgbuf
{
    long mtype;//消息标识
    char mtext[1];//消息内容
}
msgsz:消息正文的字节数,消息长度
msgflg选项:IPC_NOWAIT:非阻塞发送    0:阻塞发送
返回值:成功返回0,失败返回-1

msgflg选项0:阻塞发送。当所发送到的消息队列已满,则消息队列发送函数就会阻塞当前的进程。当消息队列有可以放得下所发送消息的空间,msgflg函数才会发送消息成功。

msgflg选项IPC_NOWAIT:即使消息队列已满,msgflg函数也不会阻塞当前进程,会马上返回。

  • 接受特定类型消息(通过msgrcv()函数来实现)
msgrcv函数:从消息队列读取消息
函数原型:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:msqid:消息队列标识符。
msgp:存放消息的结构体,结构体类型要与 msgsnd() 函数发送的类型相同
msgsz:要接收消息的大小

msgtyp 有多个可选的值:(消息缓存区第一行的消息标识)
-0 表示接收第一个消息,
-大于 0 表示接收类型等于 msgtyp 的第一个消息
-小于 0 则表示接收类型等于或者小于 msgtyp 绝对值的第一个消息

msgflg 用于设置接收的处理方式,取值情况如下:
– 0: 阻塞读取消息,没有该类型的消息 msgrcv 函数一直阻塞等待
– IPC_NOWAIT:非阻塞读取,无可读消息立即返回
– IPC_NOERROR:队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分被丢弃

返回值:如果接收消息成功返回接收到的消息的长度(字节数),否则返回-1

msgrcv() 函数解除阻塞的条件也有三个:

  • 消息队列中有了满足条件的消息。
  • msqid 代表的消息队列被删除。
  • 调用 msgrcv() 函数的进程被信号中断。
  • 删除消息队列(通过msgctl()函数来实现)
msgctl函数:设置或者获取消息队列的相关属性,也可删除消息队列
函数原型:int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:msqid:消息队列的ID
cmd参数有三个选项:
    IPC_STAT:获取消息队列的属性信息,此时msgctl函数读取消息队列的属性然后填充到buf里面
    IPC_SET:设置消息队列的属性,此时就要提前填充好buf的值然后传给msgctl函数
    IPC_RMID:删除消息队列
*buf参数:指向相关结构体缓冲区的指针

Systme-V 消息队列实验程序

发送消息

//利用终端发送消息,quit退出
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 512
 
struct message    //定义一个结构体存放发送和读取的消息的内容的
{
	long msg_type;    //表示消息的类型
	char msg_text[BUFFER_SIZE];    //数组大小512字节
};
int main()
{
	int qid;
	key_t key;
	struct message msg;
	
	if( (key=ftok("/tmp",11)) == -1 )//根据不同的路经和关键字产生标准的key
	{
		printf("ftok failed\n");	//生成key失败
		exit(1);
	}
	
	if( (qid=msgget(key,IPC_CREAT|0666)) == -1 )//创建消息队列并得到ID【可读可写】
	{
		printf("msgget failed\n");	//创建消息队列失败
		exit(1);
	}
	printf("Open queue %d success\n",qid);
	while(1)
	{	//fgets函数从控制台终端读取数据填充至msg结构体的meg_text成员变量里面去
        //上面定义的struct message类型的结构体msg
		printf("Enter some message to the queue:");
		if( (fgets(msg.msg_text,BUFFER_SIZE,stdin)) == NULL )
		{
			puts("no message");
			exit(1);
		}
		msg.msg_type = getpid();
		//添加消息到消息队列【消息发送失败则阻塞在这里,成功的话就已经把消息添加到消息队列了】
		if( (msgsnd(qid,&msg,strlen(msg.msg_text),0)) < 0)
		{
			printf("message posted");
			exit(1);
		}
		if( strncmp(msg.msg_text,"quit",4) == 0 )//在终端输入 quit 结束进程
			break;
	}
}

 读取消息

//读取消息并打印在终端,quit退出
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 512
 
struct message    
{
	long msg_type;
	char msg_text[BUFFER_SIZE];
};
int main()
{
	int qid;
	key_t key;
	struct message msg;
	//根据不同的路经和关键字产生标准的key
	if( (key=ftok("/tmp",11)) == -1 )	//参数必须与发送程序一致
	{
		printf("ftok failed\n");	//生成key失败
		exit(1);
	}
	
	if( (qid=msgget(key,IPC_CREAT|0666)) == -1 )//创建消息队列并得到ID【可读可写】
	{
		printf("msgget failed\n");	//创建消息队列失败
		exit(1);
	}
	printf("Open queue %d success\n",qid);
	do
	{	
		//读取消息队列【0--什么数据都要  0--阻塞读取】
		memset(msg.msg_text,0,BUFFER_SIZE);
		if( msgrcv(qid,(void*)&msg,BUFFER_SIZE,0,0) < 0 )
		{
			printf("message received");
			exit(1);
		}
		printf("The message from process %ld : %s",msg.msg_type,msg.msg_text);
 
	}while( strncmp(msg.msg_text,"quit",4) );
 
	//从系统内核中移走消息队列
	if( (msgctl(qid,IPC_RMID,NULL)) < 0 )
	{
		printf("msgctl");
		exit(1);
	}
	exit(0);
}

Systme-V 消息队列实验现象

Systme-V 信号量

Systme-V 信号量本质上就是一个计数器,用来记录Linux系统里面可以访问的共享资源的次数。它用于协调多进程间对共享数据对象的读取,不以传送数据为主要目的。它主要用来保护共享资源(信号量也属于临界资源),使得该临界资源在一个时刻只有一个进程独享。

使用信号量对共享资源进行保护之后,在进程里面访问共享资源的话,首先要去判断信号量的值,假如信号量的值大于0,进程就可以访问共享资源,在访问共享资源期间,对信号量减一,等访问完共享资源再对信号量加一恢复信号量原来的值。当信号量值减到0之后,进程就不可再访问共享资源。

根据上面信号量这种机制,我们就可在进程里面实现共享资源的互斥访问和同步访问。

互斥访问:在一个时刻只允许一个进程访问共享资源,在此期间其它进程不可访问共享资源。目的是防止多个线程或进程同时访问或修改同一个共享资源导致数据不一致或竞争条件的发生。

同步访问:协调多个线程或进程之间的操作,保证它们按照一定的先后顺序执行。在并发编程中,多个线程或进程可能需要协同工作来完成特定的任务,但是它们的执行速度可能不同,导致执行顺序不确定。同步机制可以用来确保线程或进程按照特定的顺序执行,以避免竞争条件、数据竞争或意外结果的发生。

总结起来,互斥访问共享资源用于保证一次只有一个线程或进程能够访问共享资源,防止数据竞争和不一致性。同步访问共享资源用于协调多个线程或进程的执行顺序,以保证它们按照特定的顺序进行操作。这两种机制都是为了确保多个线程或进程之间的安全性和可靠性。

Systme-V 信号量用法

  • 定义一个唯一key(使用ftok()函数)

与上面消息队列相同

  • 构造一个信号量(使用semget()函数)
semget函数作用:用来获取信号量id或者创建信号量的
头文件:#include <sys/types.h>    #include <sys/ipc.h>    #include <sys/sem.h>
函数原型:int semget(key_t key, int nsems, int semflg);
参数:key:信号量键值    nsems:信号量数量
semflg:IPC_CREATE:信号不存在则创建,创建时候需要设置:mode:信号量的权限
返回值:成功返回信号量ID;失败返回-1

接下来就可以通过信号量的ID来对信号量进行加减操作。 

  • 初始化信号量(使用semctl()函数来实现)
semctl用来获取或设置信号量的相关属性,它还可以用来初始化信号量的值
函数原型:int semctl(int semid, int semnum, int cmd, union semun arg);
参数:semid:信号量ID        参数semnum:信号量编号,表示信号量集中的第 semnum 个信号量
参数cmd选项:
IPC_STAT:获取信号量的属性信息
IPC_SET:设置信号量的属性
IPC_RMID:删除信号量
IPC_SETVAL:设置信号量的值
参数arg://此位置参数不是一成不变的,用联合体来表示参数的多样性
union semun
{
    int val;
    struct semid_ds *buf;
}
返回值:成功,由cmd类型决定;失败返回-1
  • 对信号量进行P/V操作,实际就是加 / 减操作(使用semop()函数)
semop函数:对信号量进行加减操作
函数原型:int semop(int semid, struct sembuf *sops, size_t nsops);
参数:semid:信号量的id
参数:sops:信号量操作结构体数组
struct sembuf    
{
    short sem_num;//想操作的信号量编号
    short sem_op;//信号量加减操作
    short sem_flg;//信号量行为,SEM_UNDO:如果进程结束前还没释放信号量的话,加了此标志之后,操作系统就会自动释放信号量。
}
参数:nsops:要操作的信号量数量
返回值:成功返回0;失败返回-1
  • 删除信号量(同样使用semctl()函数,cmd参数设置为RMID即可)

Systme-V 信号量实验程序

利用信号量实现进程同步试验

//sem.h文件
int init_sem(int sem_id,int init_value);
int del_sem(int sem_id);
int sem_p(int sem_id);
int sem_v(int sem_id);
//sem.c文件封装信号量底层函数
#include <sys/ipc.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/sem.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
 
union semun
{
	int val;
	struct semid_ds *buf;
};

int init_sem(int sem_id,int init_value)  //初始化信号量,给信号量sem_id赋值init_value
{
	union semun sem_union;
	sem_union.val = init_value;
 
	//给编号为0的信号量赋值
	if( semctl(sem_id,0,SETVAL,sem_union) == -1 )
	{
		printf("Initialize semaphore\n");
		return -1;
	}
	return 0;
}
int del_sem(int sem_id)    //删除信号量
{
	union semun sem_union;
	if( semctl(sem_id,0,IPC_RMID,sem_union) == -1 )
	{
		perror("Delete semaphore");
		return -1;
	}
}
//P操作,即减操作
int sem_p(int sem_id)
{
	struct sembuf sops;
	sops.sem_num =0;		//单个信号量的编号为0
	sops.sem_op = -1;		//表示P操作,每次调用sem_p函数,信号量的值减小1
	sops.sem_flg = SEM_UNDO;	//系统自动释放系统中残留的信号量
 
	if( semop(sem_id,&sops,1) == -1 )
	{
		perror("P operation");
		return -1;
	}
	return 0;
}
//V操作,即信号量加操作
int sem_v(int sem_id)
{
	struct sembuf sops;
	sops.sem_num =0;		//单个信号量的编号为0
	sops.sem_op = 1;		//表示V操作
	sops.sem_flg = SEM_UNDO;	//系统自动释放系统中残留的信号量
 
	if( semop(sem_id,&sops,1) == -1 )
	{
		perror("V operation");
		return -1;
	}
	return 0;
}
//test.c文件,在此文件里就要使用上面sem.c文件里面所封装的函数接口了
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/sem.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <unistd.h>
 
#include "sem.h"
#define DELAY_TIME 3
 
int main(void)
{
	pid_t result;
	int sem_id;
	//创建1个信号量【没有使用ftok函数,自定义6666,权限可读可写】
	sem_id = semget( (key_t)6666,1,0666|IPC_CREAT );
	init_sem(sem_id,0);

	result = fork();
	if(result==-1)
		perror("Fork\n");
	else if(result==0)		//子进程
	{
		printf("Child process will wait for some seconds...\n");
		sleep(DELAY_TIME);
		printf("The child process is running...\n");
		sem_v(sem_id);//信号量的值加一
	}
	else				//父进程
	{
		sem_p(sem_id);//因为信号量的值初始化为0,因此无法进行信号量值减一操作,父进程阻塞在此
		//等到子进程执行到信号量加一操作之后,信号量有值了,父进程才能继续运行
        printf("The father process is running ...\n");
		sem_v(sem_id);
 
		del_sem(sem_id);//删除信号量
	}
	exit(0);
}

Systme-V 信号量实验现象

Systme-V 共享内存

Systme-V 共享内存主要作用

允许多个不相关的进程访问同一个逻辑内存,直接将一块裸露的内存放在需要数据传输的进程面前,让它们自己使用。因此,共享内存是效率最高的一种 IPC 通信机制,它可以在多个进程之间共享和传递数据,进程间需要共享的数据被放在共享内存区域,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去,因此所有进程都可以访问共享内存中的地址,就好像它们是由用 C 语言函数 malloc 分配的内存一样。

Systme-V 共享内存特点

当进程 1 在读取共享内存的数据时,进程 2 却修改了共享内存中的数据,那么必然会造成数据的混乱,进程 1 读取到的数据就是错误的。因此,共享内存是属于临界资源,在某一时刻最多只能有一个进程对其操作(读/写数据),共享内存一般不能单独使用,而要配合信号量、互斥锁等协调机制,让各个进程在高效交换数据的同时,不会发生数据践踏、破坏等意外。

共享内存的思想非常简单,进程与进程之间虚拟内存空间本来相互独立,不能互相访问的,但是可以通过某些方式,使得相同的一块物理内存多次映射到不同的进程虚拟空间之中,这样效果就相当于多个进程的虚拟内存空间部分重叠在一起,如下图所示:


当进程 1 向共享内存写入数据后,共享内存的数据就变化了,那么进程 2 就能立即读取到变化了
的数据,而这中间并未经过内核的拷贝,因此效率极高。

总的来说共享内存有以下特点:

  1. 共享内存是进程间通信中效率最高的方式之一。
  2. 共享内存是系统出于多个进程之间通讯的考虑,而预留的的一块内存区,因此共享内存是以传输数据为目的的。
  3. 共享内存允许两个或更多进程访问同一块内存,当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。
  4. 共享内存无同步无互斥。

Systme-V 共享内存优缺点

  • 优点:使用共享内存进行进程间的通信非常方便,而且函数的接口也简单,数据的共享使进程间的数据不用传送,而是直接访问内存,加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的“血缘”关系,是系统中的任意进程都可以对共享内存进行读写操作。
  • 缺点:共享内存没有提供同步的机制,这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段(如信号量、互斥量等)来进行进程间的同步工作。

Systme-V 共享内存用法

  • 定义一个唯一的key(ftok函数)

消息对列和信号量部分有介绍ftok函数

  • 构造一个共享内存对象(使用fhmget()函数)
shmget函数功能:获取共享内存ID
函数原型:int shmget(key_t key, size_t size, int shmflg);
参数key:共享内存键值
参数size:共享内存大小(映射的内存大小)
参数shmflg:IPC_CREATE:共享内存不存在则创建并设置共享内存的权限mode
返回值:成功返回共享内存的ID,失败返回-1
  • 共享内存映射(使用shmat()函数)
系统提供的 shmat() 函数就是把共享内存区对象映射到调用进程的地址空间。

函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
参数shmid:共享内存ID
参数*shmaddr:指向映射地址的指针,NULL为自动分配
参数shmflg两个选项:SHM_RDONLY:只读方式映射    0:可读可写方式映射    注:无法以只写方式映射
返回值:成功返回共享内存的首地址,失败返回-1
  • 解除共享内存映射(使用shmdt()函数)
shmdt() 函数是用来解除进程与共享内存之间的映射的
函数原型:int shmdt(const void *shmaddr);
参数 *shmaddr:指向映射的共享内存的起始地址的指针。
参数:调用成功返回 0,如果出错则返回-1

注意:该函数并不删除所指定的共享内存区,只是将先前用 shmat() 函数映射好的共享内存脱离当前进程,共享内存还是存在于物理内存中。

  • 删除共享内存(使用shmctl()函数的RMID选项)
shmctl() 用于获取或者设置共享内存的相关属性,也可以删除共享内存
函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数shmid:共享内存的ID
参数cmd:设置shmctl()函数具体功能的,有以下选项:
        IPC_STAT:获取属性信息,放置到 buf 中。
        IPC_SET:设置属性信息为 buf 指向的内容。
        IPC_RMID:删除这该共享内存。
参数*buf:属性缓冲区
返回值:成功由cmd参数的类型决定;失败返回-1

Systme-V 共享内存实验程序

实验实现利用信号量实现进程同步,通过共享内存传输数据。程序与上面信号量实验程序相比只对test.c文件进行了修改。如下更改后的test.c的代码:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <unistd.h>
 
#include "sem.h"
#define DELAY_TIME 3
 
int main(void)
{
	pid_t result;
	int sem_id;
	int shm_id;
	char *addr;
	//创建1个信号量【没有使用ftok函数,自定义6666,权限可读可写】
	sem_id = semget( (key_t)6666,1,0666|IPC_CREAT );
	//创建1个共享内存对象【没有使用ftok函数,自定义7777,权限可读可写】
	shm_id = shmget( (key_t)7777,1024,0666|IPC_CREAT );
 
	init_sem(sem_id,0);
 
	result = fork();
	if(result==-1)
		perror("Fork\n");
	else if(result==0)		//子进程
	{
		printf("Child process will wait for some seconds...\n");
		sleep(DELAY_TIME);
		//映射共享内存【NULL--映射地址自动设置,0--可读可写】
		addr = shmat(shm_id,NULL,0);
		if(addr==(void *)-1)
		{
			printf("Shmat111 error\n");
			exit(-1);
		}
		
		memcpy(addr,"helloworld",11);//设置共享内存中的内容,将“helloworld”拷贝到共享内存里
 
		printf("The child process is running...\n");
		sem_v(sem_id);
	}
	else				//父进程
	{
		sem_p(sem_id);//在此申请不到信号量,必须等子进程中调用完sem_v函数信号量加一。
		printf("The father process is running ...\n");
		//映射共享内存地址
		addr = shmat(shm_id,NULL,0);
		if(addr==(void *)-1)
		{
			printf("Shmat222 error\n");
			exit(-1);
		}
		printf("Shared memory string:%s\n",addr);//打印出子进程写入的“helloworld”
		//解除共享内存映射
		shmdt(addr);
 
		shmctl(shm_id,IPC_RMID,NULL);
 
		sem_v(sem_id);
 
		del_sem(sem_id);
	}
	exit(0);
}

Systme-V 共享内存实验现象

写在最后:操作系统原理知识很多,本文以及本篇紧接着的上一篇博文只是对其一些基础以及常用到的知识点进行了记录,这两篇博文有助于对操作系统原理有个大体框架的认知。希望本文可以帮助到各位读者,文章如有不足,欢迎大家指出,如果文章帮到你了,请一定帮忙点个赞哦。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值