引言:IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
一、管道
概念:管道分为无名管道和命名管道。管道是一种半双工的通信方式(半双工即信息既可由A传到B,又能由B传A,但只能由一个方向上的传输存在),数据只能单向流动。管道的问题在于他们没有名字,只能在具有亲缘关系(父子进程间)的进程间使用。
管道特点:
①半双工的通信方式。
②只能用于具有亲缘关系的进程间的通信。
③管道不储存数据,只存在于内存。
1、无名管道
函数原型:
#include <unistd.h>
int pipe(int fd[2]);
返回值为整形,若成功则范围0,失败则返回1.
其中fd[0]为只读打开,fd[1]为只写打开.
2、命名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
返回值为整形,若成功则范围0,失败则返回1.
参数const char *pathname:路径名
mode_t mode:一般使用0600(可读可写)
注意:命名管道一般与open、read、write合用进行传送消息.
二、消息队列
什么是消息队列?
答:消息队列可以理解是一个存放消息的容器。将消息写入消息队列,再将消息从消息队列中取出,一般来说是先进先出的顺序。可以解决两个进程的读写速度不同(处理数据速度不同)等原因,而且消息队列里的消息哪怕进程崩溃了也不会消息。
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
特点:
①消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
②消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
③消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
#include <sys/msg.h>
1.ftok函数生成键值
key_t ftok(const char *pathname, int proj_id); //每一个消息队列都有一个相应的键值(key)关联
参数:
pathname:一个已存在的路径名
proj_id:0-255之间的数值,代表项目ID,可自取
2.创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);
参数:
key:相当于索引(可用ftok函数获取key值),通过key在linux找到相应的队列。
flag:所需要的操作和权限,可以用来控制创建一个消息队列
3.添加消息:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
4.读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
5.控制消息队列(队列移除):成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
eg:msgctl(msgID,IPC_RMID,NULL);
消息数据结构体
struct msgbuf {
long mtype; /* 类,消息队列可以控制读取相应的数据 */
char mtext[128]; /* 数据,传递的数据存放在这里 */
};
三、共享内存(两个或多个进程共享一个给定的存储区)
什么是共享内存?
答:共享内存,顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存,进程可以将同一段物理内存连接到他们自己的地址空间中,所以的进程都可以访问共享内存的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问的同一段共享内存的任何其他进程。
共享内存的通信原理
答:在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。
如上图:当两个进程通过页表将虚拟地址映射到物理地址时,在物理地址中有一块共同的内存区,即共享内存,这块内存可以被两个进程同时看到。这样当一个进程进行写操作,另一个进程读操作就可以实现进程间通信。但是,我们要确保一个进程在写的时候不能被读,因此我们使用信号量来实现同步与互斥。
为什么说使用共享内存通信速度最快?
答:如上图,进程ProcA给内存中写数据,ProcB从内存中读取数据,在此期间一共发送了两次赋值
①ProcA到共享内存 ②内存到ProcB;因为直接在内存上操作,所以共享内存的速度也就提高了。
特点:
①共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
②多个进程可以同时操作,需要进行同步。
③信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
#include <sys/shm.h>
1. 创建或获取一个共享内存:
int shmget(key_t key, size_t size, int shmflg);
参数:
key:有frok生产的key标识,标识系统的唯一IPC资源。
size:需要申请共享内存的大小。(在操作系统中,申请内存的最小单位是页,一页是4k字节,为避免内存碎片,我们一般申请的内存大小为页的整数倍)
shmflg:如果要创建新的共享内存,需要使用IPC_CREAT,IPC_EXCL,如果是已经存在的,可以使用IPC_CREAT或直接传0
返回值:成功时返回一个新建或已经存在的共享内存标识符,取决于shmflg的参数。
失败返回-1并设置错误码。
2 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *shmaddr, int shmflag);
参数:
shm_id:共享存储段的标识符(shmget的返回值)
shmaddr:若shmaddr=0,则存储段连接到内核选择的第一个地址上(推荐)
shmflg:默认已读写连接此段
返回值:成功返回共享存储段的指针(虚拟地址),并且内核将其与共享存储段相关的shmid_ds结构中的shm_nattch计数器+1(类似于引用计数)
错误返回-1
eg:shamat(shmID,0,0) //让内核自动安排共享内存,并具备可读可写.
3 断开与共享内存的连接:
int shmdt(void *addr);
注意:该函数并不删除所指定的共享内存区,而是将之前用shmat函数连接好的共享内存区脱离目前的进程。
返回值:成功返回0,失败返回-1
4 销毁共享内存:
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
参数:
shm_id:共享内存段标识符
cmd:指定的执行操作,设置为IPC_RMID时表示可以删除共享内存
*buf:设置为NULL即可
返回值:成功返回0,失败返回-1
eg:shmctl(shmID,IPC_RMID,NULL);
可以使用命令:
查看我们创建的共享内存ipcs -m
删除共享内存ipcrm -m 'shmid'
实现共享内存编程思路:
①创建共享内存 shmget()
②映射 shmat()
③进行数据交换(可直接对地址进行赋值strcpy)。
④释放共享内存 shmdt()
⑤关闭 shmctl()
四、信号
1、信号的处理:
信号的处理有三种方法,分别是:忽略、捕捉和默认动作
忽略信号:大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL和SIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景
捕捉信号:需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
系统默认动作:对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。
具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。在此,我就不详细展开了,需要查看的,可以自行查看。
2、信号的使用:
kill -信号编号 进程id号
3、信号处理函数
①入门版signal
发送指令kill
int kill(pid_t pid, int sig);
例子:
/*
使用: ./signal signalnum pid
*/
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
int main(int agrc,char **agrv)
{
//int kill(pid_t pid, int sig);
int pid;
int signum;
signum=atoi(agrv[1]); //atio:把字符串转换成整型数
pid=atoi(agrv[2]);
printf("num=%d,pid=%d\n",signum,pid);
kill(pid,signum);
printf("send signal ok\n");
return 0;
}
捕捉信号,改变它原本的操作
//函数原型:
sighandler_t signal(int signum, sighandler_t handler);
参数:
signum:捕捉的信号
handler:信号处理函数
例子:
/*
捕捉信号,改变信号原来的操作
*/
#include <signal.h>
#include <stdio.h>
void hander(int signum)
{
printf("signnum=%d\n",signum);
switch(signum){
case 2:
printf("SIGINT\n");
break;
case 9:
printf("SIGKILL\n");
break;
case 10:
printf("SIGUSR1\n");
break;
}
}
int main()
{
signal(SIGINT,hander);
signal(SIGKILL,hander);
signal(SIGUSR1,hander);
while(1);
return 0;
}
②高级版signaction()接收信号
#include <signal.h>
int sigaction (int signum, const struct sigaction *act,structsigaction*oldact);
第一个参数为:捕捉的信号
第二个参数为:绑定某些功能参数的结构体
第三个参数:备份
struct sigaction {
void (*sa_handler)(int); //信号处理程序,不接手额外数据
void (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,接收额外数据
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
第一个参数为:函数指针1
第二个参数为:函数指针2
第三个参数为:结构体
第四个参数为:标记
如何发送信号?
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
第一个参数为:发给谁
第二个参数为:什么信号
第三个参数为:发送的消息(int或char)
五、信号量
1、信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
特点:
①信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
②信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
③每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
④支持信号量组
#include <sys/sem.h>
1.创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
第二个参数为:信号量集的个数
2.对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
3.控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
第二个参数为:操作第几个信号量
第四个参数需定义一个联合体.
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
(Linux-specific) */
};
2、PV操作
int semop(int semid, struct sembuf *sops, unsigned nsops);
第二个参数为:配置信号量个数
第三个参数为:指针,指向第二个信号量的地址。
```c
key_t key;
key=ftok(".",1);
//int semget(key_t key, int nsems, int semflg);
int semid=semget(key,1,IPC_CREAT|0666); //获取信号量
union semun initsem;
initsem.val=0; //操作第0个信号的值
//int semctl(int semid, int semnum, int cmd, ...);
semctl(semid,0,SETVAL,initsem);//SETVAL设置信号量的值
取钥匙
```c
void pGetKey(int id)
{
struct sembuf set;
set.sem_num=0;
set.sem_op=-1;
set.sem_flg=SEM_UNDO;
semop(id,&set,1);
printf("getkey\n");
}
放钥匙
void vPutBackKey(int id)
{
struct sembuf set;
set.sem_num=0;
set.sem_op=1;
set.sem_flg=SEM_UNDO;
semop(id,&set,1);
printf("put back the key\n");
}