Linux进程间通信(共享内存&信号&信号量)

目录

前言

一、共享内存

共享内存补充

二、信号

信号概述

1.信号的名字和编号

2.信号的处理

信号相关函数入门版

信号处理函数的注册

信号处理发送函数

1.使用signal函数实现Ctrl+C无法终止进程

2.使用kill函数在程序内部执行kill指令

信号相关函数高级版

信号处理函数的注册pro

信号处理发送函数pro

代码示例

三、信号量

信号量概述

信号量相关函数

代码示例

总结


前言

我的上一篇文章Linux进程间通信讲了IPC(进程间通信)的三种方式,管道、FIFO和消息队列,但是它们各有各的优缺点,本文会讲解IPC的另外三种方式。


一、共享内存

相比于前三个IPC方式,共享内存有什么不同?
我们可以假设两个人要进行交流,管道和FIFO就好像两人中间有一个水管,一方往里面放,另一方就只能拿;消息队列就好像一个人往箱子里面放纸条,另一个人从箱子里拿出纸条,读取完后再把纸条放回去(消息读完后不会消失,不同于管道);而共享内存就像两人中间有一张桌子,一个人往桌子上写东西,另一个人可以直接看到它写的(桌子对于两个人来说是共用的)。

由名字可知,两个进程可以挂载同一个内存空间,这个内存空间是共享的。

相关函数:

#include <sys/shm.h>
//创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int flag);

//连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);

//断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr);

//控制共享内存的相关信息:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

关于shmget函数的第一个参数,我的上一篇博客是直接手动输入一个32位的整数,这里要用ftok函数获取唯一的一个键值:

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

key_t ftok(const char *pathname, int proj_id);

参数:
1.
函数的第一个参数是一个路径名,通常是一个存在的文件路径,待会的代码示例中会传入".",这意味着 ftok 函数会使用当前进程的工作目录(即程序运行时所在的目录)作为路径来生成键值,我们只需要知道怎么使用就行了。
2.第二个参数proj_id,它用于进一步区分同一路径下不同对象(如消息队列、信号量、共享内存)的键值。在使用ftok函数时,传入的proj_id值应当是一个非零的整数。简单理解就是这个数字可以被视为一个简单的标识符,用于区分同一路径下不同的对象。

代码示例:

shmwrite.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main()
{
	int shmid;
	char *shmaddr;

	key_t key;
	key=ftok(".",1);//获取键值

	shmid=shmget(key,1024*4,IPC_CREAT|0666);//创建一个共享内存,权限为可读可写,大小为4兆
	if(shmid==-1)//创建/获取共享内存失败
	{
		printf("shmget failed\n");
		exit(-1);
	}	

	shmaddr=shmat(shmid,0,0);//挂载共享内存,获取地址

	strcpy(shmaddr,"hello!\n");//将字符串复制到共享内存里

	sleep(5);//睡眠五秒

	shmdt(shmaddr);//取消挂载/卸载共享内存
	shmctl(shmid,IPC_RMID,0);//删除共享内存

	return 0;
}

下面有几项要注意的地方:
1.创建共享内存时,空间大小必须以为单位,即1024字节,shmget函数的第二个参数一般传入IPC_CREAT,还需要|上创建的权限(0666表示可读可写,0777表示可读可写可执行)。
2.挂载共享内存shmat的第二和第三个参数通常写0即可,第二个参数写0表示让Linux内核为我们自动安排共享内存,第三个表示挂载/映射的共享内存为可读可写

3.往共享内存里写数据用strcpy函数,需要包含string.h头文件,它的功能是将一个字符串复制到另一个字符串,由于我们定义的共享内存指针是char型的,所以可以用这个函数。
4.删除共享内存的第三个参数通常写0,表示不接收删除共享内存的信息等。


shmread.c

读和写的代码其实大差不差,读共享内存可以直接用printf函数,传入共享内存指针即可。

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main()
{
	int shmid;
	char *shmaddr;

	key_t key;
	key=ftok(".",1);

	shmid=shmget(key,0,0);//前面创建了共享内存,这里直接获取id就行了,所以后面参数写0
	if(shmid==-1)
	{
		printf("shmget failed\n");
		exit(-1);
	}	

	shmaddr=shmat(shmid,0,0);

	printf("from shm: %s",shmaddr);//传入指针

	shmdt(shmaddr);//卸载

	return 0;
}

读的代码不需要删除共享内存,由写的代码来完成,先执行写的代码,睡眠五秒后会删除共享内存空间。

运行结果:

先执行w,在5秒内执行r,成功打印信息,之后再执行r,由于没有创建共享内存,所以打印错误信息。

共享内存补充

在终端输入指令 ipcs -m 来查看系统中有哪些共享内存

输入 ipcrm -m shmid shmid为共享内存的ID,用于删除共享内存

共享内存有个小缺陷,就是两个人不能同时写,不然数据会混在一起,所以共享内存一般都结合信号量来使用,一个人写的时候另一个人只能看


二、信号

信号概述

1.信号的名字和编号

每个信号都有一个名字和编号,这些名字都以“SIG"开头,例如"SIGIO","SIGCHLD"等等。

信号定义在 signal.h 头文件中,信号名都定义为正整数。

具体的信号名称可以使用 kill -l 来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kil对于信号0有特殊的应用。

(使用 kill -l 指令查看信号的名字以及序号) 

假设我们写了一个无限循环的程序,想要让它停止运行,我们会按 Ctrl+C 键来终止进程,其实就是向进程发送了第2个信号SIGINT

还有一种杀死进程的方式,另外打开一个终端,输入 ps -aux|greq 可执行文件名 来查找正在运行的进程ID,再输入 kill -9 进程ID 即可杀死进程,并且在终端打印Killedkill是发送信号的指令,-9表示发送第9个信号,也就是发送SIGKILL信号给对应ID的进程。以上说的这些指令都需要我们掌握,因为后面的代码示例都是根据这些指令来写的。

2.信号的处理

信号的处理有三种方法,分别是:忽略捕捉默认动作

·忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILLSIGSTOP )。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景。

·捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。

·系统默认动作,对于每个信号来说,系统都对应有默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。

总的来说,信号就是一个软中断,和我们单片机一样,某个动作出发了中断,进而执行中断里面的内容(硬件中断)。 

信号相关函数入门版

信号处理函数的注册

#include <signal.h>

//它定义了一个名为sighandler_t的新类型(typedef),该类型是一个指向函数的指针,这个函数接受一个整型参数 int,并且没有返回值(void)
typedef void (*sighandler_t)(int)

sighandler_t signal(int signum, sighandler_t handler);

signal就是我们要使用的函数,第二个参数传入我们自定义的函数的函数名。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

信号处理发送函数

#include <signal.h>
#include <sys/types.h>

int kill(pid_t pid, int siq);//第一个参数传入进程ID,第二个参数传入信号的编号

 这个kill函数实现的功能其实和kill指令一样,但运用场景不同。话不多说我们上代码示例会更好理解。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

1.使用signal函数实现Ctrl+C无法终止进程

#include <stdio.h>
#include <signal.h>

void handler(int signum)//自定义函数,参数为整形,无返回值
{
	printf("get signum=%d\n",signum);//打印信号编码
	printf("never quit\n");
}

int main()
{
	signal(SIGINT,handler);//检测SIGINT信号(Ctrl+C),检测到了进入handler函数
	while(1);
	return 0;
}

首先看我们定义的函数,函数的参数会将触发该函数的信号的编号传进来,它由操作系统传入。

详细说明一下代码的步骤:当代码运行起来时,程序就跑到while(1)的位置了,所以信号是个软中断的说法也被证实了,当检测到SIGINT信号时,即用户按下啃臭C,就会进入用户定义的handler函数,操作系统会将信号的编号传入该函数,即使你的函数里没有写任何东西,这个信号也不会实现它原本的功能。(除了 SIGKILLSIGSTOP !!!,至于为什么看前面信号概述讲忽略信号那一部分
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

2.使用kill函数在程序内部执行kill指令

#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
	int signum;
	int pid;

	signum = atoi(argv[1]);//将字符串转换为整数类型
	pid = atoi(argv[2]);

	kill(pid,signum);
	printf("send signal ok\n");
	return 0;
}

运行结果: 

左边的终端执行死循环的程序,右边再打开一个终端,查看进程的ID后执行编译好的pro可执行文件,传入信号编号和进程ID,左边会打印Killed,和kill指令实现功能一样。 
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

信号相关函数高级版

信号处理函数的注册pro

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数:
1.信号编号,要接收哪个信号?
2.是一个指向 struct sigaction 类型的结构体指针,结构体原型为:

struct sigaction {
    void (*sa_handler)(int);           // 指定信号处理函数
    void (*sa_sigaction)(int, siginfo_t *, void *); // 详细信号处理函数
    sigset_t sa_mask;                  // 额外的要阻塞的信号集合
    int sa_flags;                      // 特殊标志
    void (*sa_restorer)(void);         // 未使用,以备将来扩展
};

!!!通常配置结构体的第二个和第四个参数即可,第四个参数设置成 SA_SIGINFO ,这表示在使用sigaction函数设置信号处理时,希望使用详细的信号处理函数 sa_sigaction 而不是简单的处理函数 sa_handler ,因为我们只设置结构体的第二个和第四个参数,所以自然选择这个配套在一起,第二个参数传入自定义的函数名即可
3.传入NULL即可

讲完了各个参数,那么现在应该编写用户自定义的函数了(这里还是有点绕的,还是需要一步步剖析源码,自己梳理一遍,等会结合代码示例来看),根据 struct sigaction 结构体的第二个参数可知,用户定义的函数原型应为:

void handler(int signum, siginfo_t *info, void *context);

参数:
1.信号的编号,由操作系统传入。
2.siginfo_t是一个结构体,它定义在 <signal.h> 头文件中,这个结构体包含了关于信号更详细的信息,使得信号处理函数能够获取有关信号发生背景的更多信息

可以看到siginfo结构体包含了很多参数,代码示例我们只需要接受这两个参数,它会将接受到的整型数放在这里,而 si_value  又是一个结构体,整型数据会存放在 si_value.sival_int  里面。
3.第三个参数用来判断空或者非空(有无收到数据)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

信号处理发送函数pro

#include <signal.h>

int sigqueue(pid_t pid, int siq, const union sigval value);

参数:
1.传入要接收信号的进程的进程ID。
2.要发送的信号的编号。
3.第三个参数是个联合体(二选一),原型如下:

union sigval {
    int sival_int;      // 整数值作为附加数据
    void *sival_ptr;    // 指针作为附加数据
};

之所以叫高级版,就是可以实现进程间收发数据,入门版的函数更像是发送某些指令,而高级版可以发送信号的同时发送数据,要么发送整形数,要么发送字符串等。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

代码示例

讲了函数的相关参数,可能看的一头雾水,但是结合示例来看,我们只用掌握一些基础的用法即可,不需要太深入的了解,等实际用到的时候再去查找相关资料就行。

proread.c

#include <stdio.h>
#include <signal.h>

void handler(int signum, siginfo_t *info, void *context)//用户自定义函数
{
	printf("get signum %d\n",signum);//打印信号编号

	if(context != NULL)//如果内容非空
	{
		printf("get data = %d\n",info->si_int);//打印收到的数据
		printf("get data = %d\n",info->si_value.sival_int);//打印收到的数据
	}
}

int main()
{
	struct sigaction act;//定义struct sigaction类型的结构体

    /*
     * 只需要配置结构体的两个参数,实现最基础的功能
     */
	act.sa_sigaction = handler;//传入自定义函数名
	act.sa_flags = SA_SIGINFO;

	sigaction(SIGUSR1,&act,NULL);//检测SIGUSR1信号,参数二传入结构体指针,参数三通常写NULL
    printf("%d\n",getpid());//打印一下进程的ID,方便write函数发送信号和数据
	while(1);//不让程序退出
	return 0;
}

只要认真看看注释,跟着上面的函数参数过一遍,应该是很好理解的,要配置的东西也不是很多,毕竟我们只实现最基本的功能,有一点要注意的是用户自定义的函数:因为第二个参数是个结构体指针,所以读取里面的数据要用 -> ,其实高级版和入门版的函数可以类比来学习,入门版自定义函数的参数会由操作系统来传入,高级版只不过我们可以获取的数据更多样罢了
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

prowrite.c

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
	int signum;
	pid_t pid;

    /*
     * 将main函数传入的字符串转换为整形数
     */
	signum = atoi(argv[1]);
	pid = atoi(argv[2]);

	union sigval value;//定义联合体
	value.sival_int = 100;//发送整形数100

	sigqueue(pid,signum,value);//第三个参数直接传入value即可

	return 0;
}

运行结果:

左边先运行read函数,此时会打印进程的ID号,右边再运行write函数,第一个参数传入信号的编号,SIGUSR1的编号是10,再传入进程ID,这时右边就会打印到收到的数据和信号编号,因为发送的是整型数,所以他会存在两个地方(见proread.c用户自定义函数部分) 


三、信号量

信号量概述

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。信号量无法实现进程间收发数据,举个例子来说明信号量的作用。

假设有一个房子,房子外有一个箱子,里面有一把钥匙,a想进入房子就要拿到钥匙然后进去,这时b也想进这个房子,但是b没有钥匙,所以b只能等a出来把钥匙放回箱子后再拿钥匙开锁进房子。那么这就涉及到信号量的几个概念。

·信号量--钥匙

·临界资源--房子

·p操作--拿到钥匙

·v操作--放回钥匙

·信号量集--箱子

以上就是信号量最主要的概念,Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作,所以就需要信号量集,有些大型的程序会管理很多个信号量,相当于信号量的集合(实际是一个数组,一个箱子可以有很多把钥匙)。

临界资源就是大家共享的资源,但是同一时间只希望一个人使用,比如共享内存,同一时间我只希望一方写数据,另一方只能读数据。以上这些操作就需要用到信号量,涉及同步与互斥的概念。

信号量相关函数

#include <sys/sem.h>
#include <sys/types.h>
#include <sys/ipc.h>

//创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int nsems, int semflg);

//对信号量数组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf *sops, size_t nsops);

//控制信号量的相关信息
int semctl(int semid, int semnum, int cmd, ...);

代码示例

先讲函数的参数个人感觉不太好理解,所以先写代码,结合代码来看为什么参数要这么设置;代码功能:正常fork出一个子进程后,往往是父进程先运行,通过信号量实现子进程先运行

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>      
#include <sys/sem.h>
#include <unistd.h>

union semun//联合体的原型
{
    int val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO */
};

/*
 * 步骤5
 */
void p(int semid)//p操作
{
	struct sembuf set;//定义结构体
	set.sem_num = 0;
	set.sem_op = -1;
	set.sem_flg = SEM_UNDO;
	semop(semid,&set,1);

	printf("get key\n");
}

/*
 * 步骤6
 */
void v(int semid)//v操作
{
    struct sembuf set;
    set.sem_num = 0;
    set.sem_op = 1;
    set.sem_flg = SEM_UNDO;
    semop(semid,&set,1);

    printf("put back key\n");
}

int main(int argc, char **argv)
{
    /*
     * 步骤1
     */
	key_t key;
	key = ftok(".",2);
	
    /*
     * 步骤2
     */
    int semid;
	semid = semget(key,1,IPC_CREAT|0666);

    /*
     * 步骤3
     */
	union semun initsem;
	initsem.val = 0;//设置联合体的val为0
	semctl(semid,0,SETVAL,initsem);//传入定义的联合体变量

    /*
     * 步骤4
     */
	int pid=fork();
	if(pid>0)//父进程
	{
		p(semid);//拿钥匙
		printf("father\n");//进房子
		v(semid);//放钥匙
	}
	else if(pid == 0)//子进程
	{
		printf("child\n");//直接进房子
		v(semid);//放钥匙
	}

	return 0;
}

我们先看main函数:
步骤1:首先用ftok函数获取唯一的键值,第二个参数写"2",因为前面的共享内存写了"1",我这两个文件是在同一个目录下的,所以要区分。

步骤2:使用 semget 函数创建一个信号量组,我们只需要一个信号量,所以第二个参数写"1",第三个参数应该很熟悉了,和创建共享内存一样,传入 IPC_CREAT 这个宏,权限 0666 表示可读可写。

步骤3:创建完信号量组,就要对其进行初始化操作。使用 semctl 函数,第一个参数传入信号量组ID,第二个参数表示初始化第几个信号量,因为是数组,所以传入"0"。重点:semctl函数可以有三个或四个参数,第四个参数是否要填取决于第三个,这里cmd传入的是SETVAL表示要初始化信号量的值,那么第四个参数就要定义一个联合体(代码开头)。用联合体类型定义完变量后,给联合体的val参数赋值为0(只用这个参数,"0"表示没钥匙,"1"表示有钥匙!!!

步骤4:fork出子进程,前面的初始化都做完了,现在来想一下代码思路:既然父进程会先执行,那我就让父进程去拿钥匙,初始化成没钥匙,那父进程就会进不去(阻塞状态),子进程不需要拿钥匙,直接进屋子(在p操作和v操作之间的代码我们看作是进屋子),子进程出来之后把钥匙放进箱子(这里不管子进程是从哪里得到的钥匙,我们开头说了,信号量实际上是个计数器,通过对信号量加减的操作来控制钥匙是否存在于箱子中),此时父进程才能拿到钥匙进入房子。

步骤5:现在来写封装p操作,需要用到 semop 函数,来对信号量进行操作,函数的第二个参数是一个结构体类型的数组,因为我们只有一个信号量,所以定义一个结构体就行了。

查看官方的示例,定义结构体后要配置三个参数,那我们也照葫芦画瓢。第一个参数是信号量的编号,我们只有一个信号量,给它0就好(看你喜欢),重点是第二个参数,信号量操作,通常是-1(P操作)、+1(V操作)或其他自定义操作,根据p或v操作后的值来判断有没有钥匙。第三个参数写 SEM_UNDO 可以增加系统的健壮性和稳定性,确保在任何情况下都能正确处理信号量的操作

步骤6:和步骤5一样,只不过放回钥匙。

至此信号量的具体使用就讲完了,来看一下运行结果:

确实实现了想要的功能。


总结

至此,进程间通信的方式都讲完了,这篇博客也是我写过的最长的一篇博客,涉及到了很多复杂的c语言代码,一套又一套,大伙可以看看注释,自己敲一遍,搞清楚步骤就不会这么难了,主要是函数比较多,很多参数不知道是用来干嘛的,我上面的示例都配置了一些常用的参数,应该能用于大部分场景了,等做到某些很复杂的项目时再去研究其他参数的作用,接下来我会更新Linux线程编程的相关内容,可以在评论区讨论一下,大家一起学习!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

sakabu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值