Linux进程间通信学习2

共享内存

相比于前三个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);

这个参数Key之间我是直接初始化为一个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值应当是一个非零的整数。简单理解就是这个数字可以被视为一个简单的标识符,用于区分同一路径下不同的对象。
运用函数小demo:
write:

#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.删除共享内存的第三个参数通常写0,表示不接收删除共享内存的信息等。

read:

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

运行结果:
在这里插入图片描述
容易误解事项:
将write函数改写为这样后,就是将sleep函数改迟一点点:
在这里插入图片描述
运行结果还是一样的,只要不删除;另外一个进程就还可以读出共享内容的信息,而不是一定要两个进程一起挂起才可以!


共享内存补充
在终端输入指令 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 |grep 可执行文件名 来查找正在运行的进程ID,再输入 kill -9 进程ID 即可杀死进程,并且在终端打印Killed,kill是发送信号的指令,-9表示发送第9个信号,也就是发送SIGKILL信号给对应ID的进程。以上说的这些指令都需要我们掌握,因为后面的代码示例都是根据这些指令来写的。
运用一下:
在这里插入图片描述

信号的处理

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

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

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

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

信号相关函数(简单)

信息处理函数的注册:

#include <signal.h>
 
//它定义了一个名为sighandler_t的新类型(typedef),该类型是一个指向函数的指针,这个函数接受一个整型参数 int,并且没有返回值(void)
typedef void (*sighandler_t)(int)
 
sighandler_t signal(int signum, sighandler_t handler);
//handler就是这个新定义的类型

参数:
signum:就是上面说的用kill -l查看的信号的名字
handler:我们要传入的函数
**
————————————————————————————————————**
信号处理发送函数:

#include <signal.h>
#include <sys/types.h>
 
int kill(pid_t pid, int siq);//第一个参数传入进程ID,第二个参数传入信号的编号

运用小demo

实现ctrl+c无法终止进程

平时我们想要认为的去终止进程,我们都是直接CTRL+C就可以了,假设我们写了一个无限循环的程序,想要让它停止运行,我们会按 Ctrl+C 键来终止进程,其实就是向进程发送了第2个信号SIGINT。
代码:

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

本来信号的处理就是系统默认动作,现在相当于我将它改写了,就是捕获这个信号SIGINT,一旦捕获到就执行我们的函数handler,这里就是简单打印了,就没有让进程退出来。

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

运行结果:
在这里插入图片描述
就相当于前面用的kill -9 +pid

信号相关函数高级版

信息处理函数的注册:

#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即可

第二个参数我们慢慢刨开来看,第二个参数是一个结构体指针,里面我们一般配置第2,4个即可,第4个已经说了,那么我们可以看出来第2个这个函数原型为:

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

参数:
1.信号的编号,由操作系统传入。
2.siginfo_t是一个结构体,它定义在 <signal.h> 头文件中,这个结构体包含了关于信号更详细的信息,使得信号处理函数能够获取有关信号发生背景的更多信息。
在这里插入图片描述
可以看到siginfo结构体包含了很多参数,代码示例我们只需要接受这两个参数,它会将接受到的整型数放在这里,而== si_value== 又是一个结构体,整型数据会存放在 si_value.sival_int 里面。
3.第三个参数用来判断空或者非空(有无收到数据)
**
————————————————————————————————————**
信号处理发送函数:

#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;    // 指针作为附加数据
};

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

运用函数小demo

就简单一个写读数据即可
read:

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

write:

#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用户自定义函数部分)
————————————————————————————————————

信号量

这个Linux的信号量和FreeRTOS的信号量差不多的,只不过是函数的不同罢了!
可以看一下这个FreeRTOS的信号量一起理解一下:FreeRTOS信号量

信号量相关函数

#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, ...);

参数:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

运用小demo:

#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 */
};
 

void p(int semid)//拿钥匙
{
	struct sembuf set;//定义结构体
	set.sem_num = 0;
	set.sem_op = -1;//拿出去-1
	set.sem_flg = SEM_UNDO;
	semop(semid,&set,1);
 
	printf("get key\n");
}

void v(int semid)//放钥匙
{
    struct sembuf set;
    set.sem_num = 0;
    set.sem_op = 1;//放进去+1
    set.sem_flg = SEM_UNDO;
    semop(semid,&set,1);
 
    printf("put back key\n");
}
 
int main(int argc, char **argv)
{
    
	key_t key;
	key = ftok(".",2);
	
    
    int semid;
	semid = semget(key,1,IPC_CREAT|0666);
 
    
	union semun initsem;
	initsem.val = 0;//设置联合体的val为0,就是盒子里面没有信号量,需要“放钥匙”
	semctl(semid,0,SETVAL,initsem);//传入定义的联合体变量
 
    
	int pid=fork();
	if(pid>0)//父进程
	{
	    /*如果是父进程先运行,里面初始化是没有信号量的,会阻塞在这里*/
		p(semid);//拿钥匙(刚开始没有,需要别人放进去才可以拿,没有拿不了)
		printf("father\n");
		v(semid);//放钥匙
	}
	else if(pid == 0)//子进程
	{
		printf("child\n");
		v(semid);//放钥匙(给父进程有的拿)
	}
 
	return 0;
}

运行结果:
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值