文章目录
共享内存
相比于前三个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;
}
运行结果: