进程间通信
- 概述
- 进程间通信方式
- 管道
- 概述
- 管道函数
- 无名管道 pipe
- 有名管道 makefifo
- 删除有名管道 rmove
- 有名管道实现 双人无序聊天 例子
- 信号
- 信号概述
- 信号处理过程
- 信号函数
- 传送信号给指定的进程 kill
- 注册信号 signal
- 查询或设置信号处理方式 sigaction
- 设置信号传送闹钟 alarm
- 有名管道+信号实现 双人无序聊天 例子
- 共享内存
- 概述
- 特性
- 共享内存命令
- 共享内存函数
- 创建\打开共享内存 shmget
- 映射共享内存 shmat
- 解除映射 shmdt
- 删除共享内存
- 消息队列
- 概述
- 消息队列函数
- 创建消息队列 msgget
- 消息队列发送消息 msgsnd
- 消息队列接收消息 msgrcv
- 删除消息队列 msgctl
- 信号量集
- 概述
- 函数
- 获取\创建信号量 semget
- 控制信号量 semctl
- 改变信号量的值 semop
- 基于进程间通信+多进程编写的银行模拟系统案例
概述
为什么进程间需要通信?为了传输数据、共享资源、通知事件、控制进程等。
那么进程间通信的原理是什么呢?进程在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信机制。
进程间通信方式
进程间通信主要有七种方式,分别是管道(有名、无名)、信号、共享内存、消息队列、信号量集以及套接字Socket(套接字后面单独介绍)。
管道
概述
无名管道只能用于父子进程或兄弟进程之间的通信,而有名管道可用于任意两进程之间通信。
管道函数
无名管道 pipe
无名管道单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存(RAM)中。
数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
头文件:
#include<unistd.h>
函数原型:int pipe(int filedes[2]);
参数介绍:
fd:一个大小为2的一个数组类型的指针。filedes[0]为管道里的读取端,filedes[1]则为管道的写入端。
返回值:若成功则返回零,否则返回-1,错误原因存于 errno 中。
只有fork函数才能创建的父子进程间才能使用无名管道。
有名管道 makefifo
命名管道是为了解决无名管道只能用于近亲进程之间通信的缺陷而设计的。命名管道是建立在实际的磁盘介质或文件系统上有自己名字的文件,任何进程可以在任何时间通过文件名或路径名与该文件建立联系。为了实现命名管道,引入了一种新的文件类型——FIFO 文件(遵循先进先出的原则)。实现一个命名管道实际上就是实现一个 FIFO 文件。命名管道一旦建立,之后它的读、写以及关闭操作都与普通管道完全相同。在文件系统中有文件名,有文件节点。
头文件:
#include<sys/types.h>
#include<sys/stat.h>
函数原型:int mkfifo(const char * pathname,mode_t mode);
参数介绍:
pathname:管道创建 路径+名字
mode:文件权限,与umask有关
返回值:若成功则返回 0,否则返回-1,错误原因存于 errno 中。
这样任意两个进程可以通过文件IO操作在其中进行数据传输。
删除有名管道 rmove
头文件:
#include<stdio.h>
函数原型:int remove(const char * pathname);
参数介绍:
pathname:管道 路径+名字
返回值:若成功则返回 0,否则返回-1,错误原因存于 errno 中。
有名管道实现 双人无序聊天 例子
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc,char *argv[])
{
//创建管道
int val1 = mkfifo(argv[1],0666);
int val2 = mkfifo(argv[2],0666);
//打开管道
int fd1 = open(argv[1],O_RDWR);
int fd2 = open(argv[2],O_RDWR);
//创建进程
pid_t pid = fork();
if( pid > 0 )//父进程
{
while(1)
{
char wb[512] = {0};
gets(wb);
write(fd1,wb,strlen(wb));
}
}
else if( pid == 0 ) //子进程
{
while(1)
{
char rb[512] = {0};
read(fd2,rb,sizeof(rb));
printf("其他:%s\n",rb);
}
}
close(fd1);
close(fd2);
return 0;
//同时在两个终端中运行,形成双管道,四进程
//父进程负责写入数据,子进程负责读出数据
//因为read和write都是阻塞的,故可以一直等待到数据变化
}
信号
信号概述
在Linux中使用 kill -l 命令可以查看到x 系统中有下 62 个信号,每一个信号都有自己独特的含义。前 31 个信号继承 unix 的非实时信号,后 31 个是 linux 自己扩展的实时信号,没有固定的含义(或者说可以由用户自由使用),所有的实时信号的默认动作都是终止进程。每一个信号用一个整型常量宏(信号编号)表示,以“SIG”开头,在系统头文件<signal.h>中定义。
信号处理过程
1 ) 信号的发生 ------ 内核进程能够发送信号(产生中断)。
2 ) 信号的接收 ------ 用户进程接收信号(保证进程不结束)。
3 ) 信号的处理 ------ 中断服务函数(信号服务函数)。
信号函数
传送信号给指定的进程 kill
头文件:
#include<sys/types.h>
#include<signal.h>
函数原型:int kill(pid_t pid,int sig);
参数介绍:
pid:目标进程pid
sig:要发送的信号(数字)
返回值:执行成功则返回 0,如果有错误则返回-1。
kill(atoi(argv[1]),atoi(argv[2]));
注册信号 signal
头文件:
#include<signal.h>
函数原型:void (signal(int signum,void( handler)(int)))(int);
即 typedef void (*sighandler_t)(int); //函数指针类型
sighandler_t signal(int signum, sighandler_t handler);
参数介绍:
下面的例子会然你看懂的
返回值:执行成功则返回 0,如果有错误则返回-1。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
typedef void (*sighandler_t)(int);
//信号服务函数
void signal_function(int num)
{
printf("2 号信号:%d\n",num);
}
int main(int argc,char *argv[])
{
//注册信号
sighandler_t val = signal(2,signal_function);
while(1);
return 0;
}
ctrl+c 信号是2号信号,所以当按下 ctrl+c 时服务函数 signal_function() 就会输出。
查询或设置信号处理方式 sigaction
头文件:
#include<signal.h>
函数原型:int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact);
参数介绍:
signum:要捕获的信号类型
act:指定新的信号处理方式
oldact:输出先前信号的处理方式
返回值:执行成功则返回 0,如果有错误则返回-1。
参数结构体 sigaction 定义如下:
struct sigaction
{
void (*sa_handler) (int); //代表新的信号处理函数
sigset_t sa_mask; //设置在处理该信号时暂时将 sa_mask 指定的信号搁置。
int sa_flags; //用来设置信号处理的其他相关操作
void (*sa_restorer) (void);// 此参数没有使用
}
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
typedef void (*sighandler_t)(int);
void signal_function(int num)
{
printf("2 号信号:%d\n",num);
}
int main(int argc,char *argv[])
{
//注册信号
struct sigaction act;
act.sa_handler =signal_function;//信号服务函数
sigemptyset(&act.sa_mask); //初始化信号集
sigaddset(&act.sa_mask,2); //添加搁置信号
sigaddset(&act.sa_mask,3); //添加搁置信号
act.sa_flags =0;
sigaction(2,&act,NULL);
while(1);
return 0;
//运行结果图和注册信号代码运行结果图一样
//当按下 ctrl+c 时服务函数signal_function()就会输出。
}
设置信号传送闹钟 alarm
头文件:
#include<unistd.h>
函数原型:unsigned int alarm(unsigned int seconds);
参数介绍:
seconds:经过指定的秒数后发送信号给当前进程。为 0,则之前设置的闹钟会被取消,并将剩下的时间返回。
返回值:返回之前闹钟的剩余秒数,如果之前未设闹钟则返回 0。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
typedef void (*sighandler_t)(int);
extern int errno;
void signal_function(int num)
{
printf("14 号信号:%d\n",num);
}
int main(int argc,char *argv[])
{
//注册信号
struct sigaction act;
act.sa_handler =signal_function;//信号服务函数
sigemptyset(&act.sa_mask); //初始化信号集
sigaddset(&act.sa_mask,2); //添加搁置信号
sigaddset(&act.sa_mask,3); //添加搁置信号
act.sa_flags =0;
sigaction(14,&act,NULL);
printf("%d\n",alarm(10));
sleep(2);
printf("%d\n",alarm(5));
while(1);
return 0;
}
在设置完10秒之后,经过两秒的睡眠还剩8秒,这时候被重新设置为五秒(上次计时还剩8秒),五秒后14号信号被发出并接收显示。
有名管道+信号实现 双人无序聊天 例子
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
typedef void (*sighandler_t)(int);
char path[10];
//信号服务函数
void signal_fun(int num)
{
int fd2 = open(path,O_RDWR); //打开读管道
char rb[512] = {0};
read(fd2,rb,sizeof(rb));
printf("%d说:%s\n",num,rb);
close(fd2); //关闭读管道
}
//关于argv:
// 1 是读通道 2 是写通道 3 是自己的信号 4 是发的信号
int main(int argc,char *argv[])
{
//init
strcpy(path,argv[2]); //保证信号服务函数能打开对应的管道
pid_t pid = getpid(); //或取自己的pid
//创建管道
int val1 = mkfifo(argv[1],0666);
int val2 = mkfifo(argv[2],0666);
//打开管道
int fd1 = open(argv[1],O_RDWR); //写管道
write(fd1,&pid,sizeof(pid_t)); //写入当前进程pid
printf("当前pid:%d\n",pid);
int fd2 = open(argv[2],O_RDWR); //读管道
read(fd2,&pid,sizeof(pid_t)); //读出要通信的pid
close(fd2);
printf("通信pid:%d\n",pid);
//注册信号
sighandler_t val = signal(atoi(argv[3]),signal_fun);
//等待写入
while(1)
{
char wb[512] = {0};
gets(wb);
write(fd1,wb,strlen(wb));
kill(pid,atoi(argv[4]));
}
close(fd1);
return 0;
//同时在两个终端中运行,形成双管道,双进程
//主函数写入管道数据并发送信号到另一个进程
//信号服务函数在接收到信号之后读取管道内容
}
共享内存
概述
指同一块物理内存被映射到进程 A、B 进程地址空间中。进程 A 可以即时看到进程 B 对共享内存中数据的更新。
特性
- 数据传输效率快,适用于对数据的速率、数量要求较高的场合(如果要求不高,一般使用消息队列)。
- 共享内存具有内存的通用特性,对共享内存执行写操作时以覆盖的方式写入,对共享内存读取数据后,内存中的数据保留,不会删除。
- 内核中的内存是不具有共享机制的,在使用共享内存前需要先创建一块共享内存(物理内存)。
- 共享内存并未提供同步机制,在一个进程结束对共享内存的写操作之前,不可以使用另外一个进程开始对它进行读取。
共享内存命令
查看内核中 IPC 对象:
ipcs -m 共享内存
-q 消息队列
-s 信号灯
删除内核中 ipc 对象:
ipcrm -m id号
共享内存函数
创建\打开共享内存 shmget
头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型:int shmget(key_t key, size_t size, int shmflg);
参数介绍:
key:共享内存标识符
size:创建共享内存空间大小
shmflg:权限标志
返回值:成功返回共享内存的标识符。失败,则返回 -1,并设置 errno 来指示错误类型。
int shmid = shmget(0x6666,512,IPC_CREAT|0644);
映射共享内存 shmat
头文件:
#include <sys/types.h>
#include <sys/shm.h>
函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
参数介绍:
shmid:共享内存的标识符。
shmaddr:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
shmflg:是一组标志位,通常为0。
返回值:调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.
char * pos = (char *)shmat(shmid,NULL,0);
解除映射 shmdt
头文件:
#include <sys/types.h>
#include <sys/shm.h>
函数原型:int shmdt(const void *shmaddr);
参数介绍:
shmaddr:指定共享内存连接到当前进程中的地址位置。
返回值:成功返回 0,失败返回-1。
shmdt(shmid);
删除共享内存
头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数介绍:
shmid:共享内存标识。
cmd:采取的操作,具体如下:
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值。
IPC_RMID:删除共享内存段。
buf:指向共享内存模式和访问权限的结构。
返回值:成功返回 0,失败返回-1。
如果需要删除共享内存,必须保证共享内存的所有连接全部断开(取消映射)后才能被真正删除。
int val = shmctl(shmid,IPC_RMID,NULL);
消息队列
概述
消息通信方式以消息缓冲区为中间介质,通信双方的发送和接收操作均以消息为单位。在存储器中,消息缓冲区被组织成队列,通常称之为消息队列。
消息队列是消息(消息内容及消息类型)的链表,存放在内核中并由消息队列标识符表示。消息队列与管道不同的是,消息队列是基于消息的,而管道是基于字节流的,且消息队列的读取不一定是先入先出,但是对于同一消息类型为先进先出。
消息队列提供了一个从一个进程向另一个进程发送数据块(结构体)的方法,每个数据块都可以被认为是有一个类型,接受者接受的数据块可以有不同的类型,接收进程根据不同类型数据进行选择接收。
消息队列函数
创建消息队列 msgget
头文件:
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
函数原型:int msgget(key_t key, int msgflg);
参数介绍:
key:消息队列的标识符
msgflg:权限标志,具体如下
IPC_CREAT //如果key不存在,则创建(类似open函数的O_CREAT)
IPC_EXCL //如果key存在,则返回失败(类似open函数的O_EXCL)
IPC_NOWAIT //如果需要等待,则直接返回错误
返回值:成功执行时,返回消息队列标识符。失败返回-1。
int msgid = msgget(0x6666, IPC_CREAT|0666);
消息队列发送消息 msgsnd
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数介绍:
msqid:消息队列的标识符。
msgp:指向要发送的消息所在的内存。
msgsz:消息的长度。
msgflg:是控制函数行为的标志,通常为0。
返回值:错误时返回-1,可以打印错误信息。正确返回 0。
struct msgbuf
{
long mtype; /* message type, must be > 0 */
char mtext[512]; /* message data */
int id;
};
struct msgbuf info;
int val = msgsnd(msgid,&info,sizeof(struct msgbuf)-sizeof(long),0)
消息队列接收消息 msgrcv
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数介绍:
msqid:消息队列的标识符。
msgp:指向要发送的消息所在的内存。
msgsz:消息的长度。
msgtyp:如果msgtype为0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息。
msgflg:是控制函数行为的标志,通常为0。
返回值:
struct msgbuf recvinfo;
ssize_t len = msgrcv(msgid,&recvinfo,sizeof(struct msgbuf)-sizeof(long), 1,0);
删除消息队列 msgctl
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型:int msgctl ( int msgqid, int cmd, struct msgid_ds *buf );
参数介绍:
msqid:消息队列的标识符。
cmd:将要采取的动作,它可以取3个值:
IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值。
IPC_RMID:删除消息队列。
buf :指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。
返回值:成功时返回0,失败时返回-1。
msgctl (msgid, IPC_RMID, NULL);
信号量集
概述
信号量集是由多个信号量组成的一个数组,作为一个整体,信号量集中的所有信号量使用同一个等待队列。Linux 的信号量集为进程请求多个资源创造了条件。Linux 规定,当进程的一个操作需要多个共享资源时,如果只成功获得了其中的部分资源,那么这个请求即告失败,进程必须立即释放所有已获得资源,以防止形成死锁。
函数
获取\创建信号量 semget
头文件:
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
函数原型:int semget(key_t key, int nsems, int semflg);
参数介绍:
key:信号量的键值
nsems:创建信号量的个数,大多数情况为1。
semflg:是一组标志,如果希望信号量不存在时创建一个新的信号量,可以和值 IPC_CREAT 做按位或操作。如果没有设置 IPC_CREAT标志并且信号量不存在,就会返错误(errno 的值为 2,No such file or directory)。
返回值:返回信号量集的标识;失败返回-1,错误原因存于 error 中。
//获取键值为 0x5000 的信号量
//如果该信号量不存在,就创建它
int semid = semget(0x5000,1,0640|IPC_CREAT);
控制信号量 semctl
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:int semctl(int semid, int sem_num, int command, …);
参数介绍:
semid:信号量标识
sem_num:是信号量集数组下标
command:对信号量操作的命令,具体如下:
IPC_RMID:销毁信号量,不需要第四个参数;
SETVAL:用来把信号量初始化为一个已知的值。
返回值:失败返回-1;如果成功,返回值比较复杂,想了解的自行搜索。
用于信号操作的共同体:
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
//销毁信号量。
semctl(semid,0,IPC_RMID);
//初始化信号量的值为 1,信号量可用。
union semun sem_union;
sem_union.val = 1;
semctl(semid,0,SETVAL,sem_union);
改变信号量的值 semop
头文件:
#include <sys/ipc.h>
#include <sys/types.h>
函数原型:int semop(int semid, struct sembuf *sops, unsigned nsops);
参数介绍:
sem_id:信号量标识。
sops:操作信号量的个数。
nsops:结构体,具体内容在下面给出。
返回值:成功返回0,失败返回-1,错误原因存于 error 中。
nsops结构体:
struct sembuf
{
short sem_num; // 信号量序号,单个信号量设置为 0。
short sem_op; //信号量操作,-1 等待操作;1 发送操作。
short sem_flg; //把此标志设置为 SEM_UNDO,操作系统将跟踪这个信号量。
};
//等待信号量的值变为 1,如果等待成功,
//立即把信号量的值置为 0
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
semop(sem_id, &sem_b, 1);
//把信号量的值置为 1。
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
semop(sem_id, &sem_b, 1);
基于进程间通信+多进程编写的银行模拟系统案例
点我~~~