1、IPC:
Inter Process Communication(进程间通信):
由于每个进程在操作系统中有独立的地址空间,它们不能像线程那样直接访问彼此的内存,所以必须通过某种方式进行通信。
常见的 IPC 方式包括:
2、无名管道:
管道的概念:
本质:
1、内核缓冲区;
2、伪文件-不占用磁盘空间;
特点:
1、读端,写端对应两个文件描述符;
2、数据写端流入,读端流出;
3、操作管理的进程被销毁之后,管道自动被释放;
4、管道默认式阻塞的;
创建匿名管道:
示例:
int pipe(int fd[2])
fd‐传出参数:
fd[0]‐读端
fd[1]‐写端
返回值:
0:成功
‐1:创建失败
实验:父子进程使用管道进行通信,实现 ps aux | grep “bash”:
使用父子进程管道通信时读端和写端不能同时打开,父进程打开写端关闭读端;
#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>
int main()
{
int ret;
int fd[2];
pid_t pid;
ret = pipe(fd);
pid = fork();
if(ret == -1)
{
printf("create pipe failed!");
exit(1);
}
//ps aux
if(pid>0)
{
close(fd[0]);//guanbi du duan da kai xie duan
dup2(fd[1],STDOUT_FILENO);
execlp("ps","ps","aux",NULL);
exit(1);
}
//grep bush
if(pid==0)
{
close(fd[1]);//guanbi xieduan da kai duduan
dup2(fd[0],STDIN_FILENO);
execlp("grep","grep","bush",NULL);
}
printf("pipe[0] is %d\n",fd[0]);
printf("pipe[1] is %d\n",fd[1]);
return 0;
}
~
结果:
3、有名管道:
示例:
int mkfifo(const char \*filename, mode_t mode)
功能: 创建管道文件;
参数: 管道文件名,权限,创建的文件权限和umask有关;
返回值: 创建成功返回0,创建失败返回-1;
特点:
有名管道;
伪文件,不占用磁盘空间;
半双工通信方式;
使用场景:
用于实现无血缘关系的进程间通信,使用mkfifo函数只会在用户区创建一个节点,不会在内核区创建管道,需要使用IO函数OPEN函数打开管道;
ret = mkfifo("/home/study/ipc/mkfifo",0755);
fd = open("./mkfifo",O_RDONLY);
实验:创建一个管道,让进程A写入进程B读出:
读进程:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
int ret;
int fd;
int nred;
char readbuf[50]={0};
//ret = mkfifo("/home/study/ipc/mufifo",0755);
if(ret == -1)
{
printf("filed!");
}
printf("create success\n");
fd = open("./mufifo",O_RDONLY);
if(fd == -1)
{
printf("open filed\n");
}
printf("open success\n");
nred = read(fd,readbuf,50);
printf("read %d byte from fifo %s\n",nred,readbuf);
printf("second read %d byte from fifo %s\n",nred,readbuf);
close(fd);
return 0;
}
~
写进程:
#include<stdio.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
char *str = "hello world";
int fd;
int ret;
ret = mkfifo("/home/study/ipc/mufifo",0755);
fd = open("./mufifo",O_WRONLY);
if(fd < 0)
{
return -1;
}
printf("open fifo success\n");
write(fd,str,strlen(str));
close(fd);
return 0;
}
结果:
注意:管道中的信息只能读取一次,读取完之后数据就被移除,但是管道还存在;
4、消息队列:
概念:
消息队列,是消息的链表,存放在内核中,一个消息队列由一个标识符(队列ID)来标识;
特点:
消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
消息队列独立于发送和接收进程,进程终止时,消息队列及其内容仍存在;
消息队列可以实现消息的随即查询,消息不一定要先进先出的次序读取,也可以按照消息类型进行读取;
两个进程间的通信:通过识别链表的ID来进行读写实现收发;
相关函数:
1、int msgget(key_t key, int msgflg);
//创建或打开消息队列,
参数:
key:和消息队列关联的key值
msgflg:是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或
操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被
忽略,而只返回一个标识符。
返回值:成功返回队列ID,失败则返回‐1,
在以下两种情况下,msgget将创建一个新的消息队列:
如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志
key参数为IPC_PRIVATE
2.int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//读取消息,成功返回消息数据的长度,失败返回‐1
参数:
msgid:消息队列的ID
msgp:指向消息的指针,常用结构体msgbuf如下:
struct msgbuf
{
long mtype; //消息类型
char mtext[N]; //消息正文
}
size:发送的消息正文你的字节数
flag:
IPC_NOWAIT 消息没有发送完成函数也会立即返回0:知道发送完成函数才返回
返回值:
成功:0
失败:‐1
3.ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
//从一个消息队列中获取消息
参数:
msgid:消息队列的ID
msgp:要接收消息的缓冲区
size:要接收的消息的字节数
msgtype:
0:接收消息队列中第一个消息
大于0:接收消息队列中第一个类型为msgtyp的消息
小于0:接收消息队列中类型值不大于msgtyp的绝对值且类型值又最小的消息。
flag:
0:若无消息函数一直阻塞
IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG。
返回值:
成功:接收到的消息i长度
出错:‐1
函数msgrcv在读取消息队列时,type参数有下面几种情况
type ==0,返回队列中的第一消息
type >0,返回队列中消息队列类型为type的第一个消息
type <0,返回队列中消息类型值小于或等于type绝对值的消息,如果有多个,则取类型值最小的消息。
可以看出,type值非0时用于以非先进先出次序读取消息,也可以把type看成优先级的权值
4.int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//控制消息队列,成功返回0,失败返回‐1
参数:
msqid:消息队列的队列ID
cmd:
IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆
盖msgid_ds的值。
IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值
IPC_RMID:删除消息队列
buf:是指向 msgid_ds 结构的指针,它指向消息队列模式和访问权限的结构
返回值:
成功:0
失败:‐1
ftok函数
key_t ftok( char * fname, int id )
//系统建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
参数:
fname就时你指定的文件名(该文件必须是存在而且可以访问的)。
id是子序号, 虽然为int,但是只有8个比特被使用(0‐255)。
返回值:
当成功执行的时候,一个key_t值将会被返回,否则 ‐1 被返回
4、共享内存:
概念:
共享内存(Shared Memory)就是允许多个进程访问同一个内存空间,是在多个进程之间共享和传递数据最高效的方式。操作系统将不同进程之间共享内存安排为同一段物理内存,进程可以将共享内存连接到它们自己的地址空间中,如果某个进程修改了共享内存中的数据,其它的进程读到的数据也将会改变;
共享内存会从内核区映射一个地址空间到用户区,需要访问用户区的地址空间才能实现读写功能;
相关函数:
1.int shmget(key_t key, size_t size, int shmflg);
//用来获取或创建共享内存
参数:
key:IPC_PRIVATE 或 ftok的返回值
size:共享内存区大小
shmflg:同open函数的权限位,也可以用8进制表示法
返回值:
成功:共享内存段标识符‐‐‐ID‐‐‐文件描述符
出错:‐1
2.void *shmat(int shm_id, const void *shm_addr, int shmflg);
//把共享内存连接映射到当前进程的地址空间
参数:
shm_id:ID号
shm_addr:映射到的地址,NULL为系统自动完成的映射
shmflg:
SHM_RDONLY共享内存只读
默认是0,表示共享内存可读写
返回值:
成功:映射后的地址
失败:NULL
3.int shmdt(const void *shmaddr);
//将进程里的地址映射删除
参数:
shmid:要操作的共享内存标识符
返回值:
成功:0
出错:‐1
4.int shmctl(int shm_id, int command, struct shmid_ds *buf);
//删除共享内存对象
参数:
shmid:要操作的共享内存标识符
cmd :
IPC_STAT (获取对象属性)‐‐‐ 实现了命令ipcs ‐m
IPC_SET (设置对象属性)
IPC_RMID (删除对象) ‐‐‐实现了命令ipcrm ‐m
buf :指定IPC_STAT/IPC_SET时用以保存/设置属性
返回值:
成功:0
出错:‐1
实验:生成共享内存,并通过标准输入写入共享内存,并读出来打印屏幕上;
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <unistd.h> // fork()
#include <sys/ipc.h> // 共享内存所需
#include <sys/shm.h> // shmget, shmat
int main()
{
int shmid;
int key;
char *p;
key = ftok("alarm.c",1);
if(key<0)
{
printf("ftok failure\n");
return -1;
}
printf("ftok success key:%x\n",key);
shmid = shmget(key,128,IPC_CREAT|0777);
if(shmid<0)
{
printf("create failure\n");
return -2;
}
printf("create succ");
system("ipcs -m");
p = (char *)shmat(shmid,NULL,0);
if(p = NULL)
{
printf("shmat funtion failure\n");
return -3;
}
//write to
fgets(p,128,stdin);
//read
printf("share data is %s",p);
return 0;
}
结果:
5、信号:
信号通信的框架:
信号通信的框架:
信号的发送(发送信号的进程):kill、raise、alarm
信号的接收(接收信号的进程):pause()、sleep、while(1)
信号的处理(接收信号的进程):signal
5.1、信号的发送(发送信号的进程):
1、kill:
#include<signal.h>
#include<sys/types.h>
函数原型:int kill(pid_t pid, int sig);
参数:
函数传入值:pid
正数:要接收信号的进程的进程号
0:信号被发送到所有和pid进程在同一个进程组的进程
‐1:信号发给所有的进程表中的进程(除了进程号最大的进程外)
sig:信号
函数返回值:成功 0 出错 ‐1
2、raise:发送信号给自己 == kill(getpid() , sig)
所需头文件:
#include<signal.h>
#include<sys/types.h>
函数原型:
int raise(int sig);
参数:
函数传入值:sig:信号
函数返回值:
成功 0 出错 ‐1
实验:raise==kill(getpid(),sig):
#include<stdio.h>
#include <signal.h>
int main()
{
printf("before sig\n");
raise(9);
printf("after sig\n");
return 0 ;
}
~
结果: 打印完“before sig”后进程执行kill动作,结束进程,不会打印“after sig”;
5.2、alarm:发送闹钟信号的函数:
5.2.1、alarm与raise函数的比较:
相同点:让内核发送信号给进程
不同点:alarm只会发送SIGALARM信号;
alarm会让内核定时一段时间在发送信号,raise会立即发送信号;
所需头文件#include <unistd.h>
函数原型 unsigned int alarm(unsigned int seconds)
参数:
seconds:指定秒数
返回值:
成功:如果调用此alarm()前,进程中已经设置了闹钟时间,则 返回上一个闹钟时间的剩余时间,否则返回0。
出错:‐1
实验:定时7秒向进程发送信号:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int i = 0;
printf("before alarm\n");
alarm(7);
printf("after alarm\n");
while(i<20)
{
i++;
sleep(1);
printf("process %d\n",i);
}
return 0;
}
结果:
在第7秒时想进程发送信号,但是由于没有设置信号处理函数对这个信号进行处理,所以这个信号会让进程终止;
信号的含义及默认操作:
信号名 | 含义 | 默认操作 |
---|---|---|
SIGHUP
|
该信号在用户终端连接
(
正常或非正常
)
结束时发出,通常是在终端的控制进程结束 时,通知同一会话内的各个作业与控制终端不再关联。
| 终止 |
SIGINT
|
该信号在用户键入
INTR
字符
(
通常是
Ctrl-C)
时发出,终端驱动程序发送此信号并送到 前台进程中的每一个进程。
| 终止 |
SIGQUIT
|
该信号和
SIGINT
类似,但由
QUIT
字符
(
通常是
Ctrl-)
来控制。
| 终止 |
SIGILL
|
该信号在一个进程企图执行一条非法指令时
(
可执行文件本身出现错误,或者试图执 行数据段、堆栈溢出时)
发出。
| 终止 |
SIGFPE
|
该信号在发生致命的算术运算错误时发出。这里不仅包括浮点运算错误,还包括溢出 及除数为0
等其它所有的算术的错误。
| 终止 |
SIGKILL
|
该信号用来立即结束程序的运行,并且不能被阻塞、处理和忽略
| 终止 |
SIGALRM
|
该信号当一个定时器到时的时候发出。
| 终止 |
SIGSTOP
|
该信号用于暂停一个进程,且不能被阻塞、处理或忽略
| 暂停进程 |
SIGTSTP
|
该信号用于暂停交互进程,用户可键入
SUSP
字符
(
通常是
Ctrl-Z)
发出这个信号
| 暂停进程 |
SIGCHLD
|
子进程改变状态时,父进程会收到这个信号
| 忽略 |
SIGABORT
|
该信号用于结束进程
| 终止 |
5.3、信号的处理:
收到信号的进程,应该怎样处理?
处理的方式:
1、进程的默认处理(内核为用户进程设置的默认处理方式)
A:忽略、B:终止进程、C:暂停
2、自己的处理方式:自己处理信号的方法告诉内核,这样你的进程收到了这个信号就会采用你自己的的处理方式
所需头文件 #include <signal.h>
函数原型 void (*signal(int signum, void (*handler)(int)))(int);
函数传入值
signum:指定信号
handler
SIG_IGN:忽略该信号。
SIG_DFL:采用系统默认方式处理信号
自定义的信号处理函数指针
函数返回值
成功:设置之前的信号处理方式
出错:‐1
signal 函数有二个参数,第一个参数是一个整形变量(信号值),第二个参数是一个函数指针,是我们自己写的处理函
数;这个函数的返回值是一个函数指针。
6、信号灯
信号灯集合(可以包含多个信号灯)IPC对象是一个信号的集合(多个信号量)
1、semget
函数原型: int semget(key_t key, int nsems, int semflg);
//创建一个新的信号量或获取一个已经存在的信号量的键值。
所需头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数参数:
key:和信号灯集关联的key值
nsems: 信号灯集中包含的信号灯数目
semflg:信号灯集的访问权限
函数返回值:
成功:信号灯集ID
出错:‐1
2、semctl:
函数原型:int semctl ( int semid, int semnum, int cmd,…union semun arg(不是地址));
//控制信号量,删除信号量或初始化信号量
所需头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数参数:
semid:信号灯集ID
semnum: 要修改的信号灯编号
cmd :
GETVAL:获取信号灯的值
SETVAL:设置信号灯的值
IPC_RMID:从系统中删除信号灯集合
函数返回值:
成功:0
出错:‐1
3、P V 操作:
int semop(int semid ,struct sembuf *sops ,size_t nsops);
//用户改变信号量的值。也就是使用资源还是释放资源使用权
包含头文件:
include<sys/sem.h>
参数:
semid : 信号量的标识码。也就是semget()的返回值
sops是一个指向结构体数组的指针。
struct sembuf{
unsigned short sem_num;//信号灯编号;
short sem_op;//对该信号量的操作。‐1 ,P操作,1 ,V操作
short sem_flg;0阻塞,1非阻塞
};
sem_op : 操作信号灯的个数
//如果其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;如果sem_op的值为负
数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用
权;如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0。
实验: 通过信号灯操作和共享内存来共同实现父子进程通信:
实现思路:
1、声明 P V 操作并进行初始化:p v操作需要配置函数,在使用P V都可以这样进行配置
void Poperation(int index,int semid)
{
struct sembuf sop;
sop.sem_num = index;
sop.sem_op = -1;
sop.sem_flag = 0;
semop(semid,&sop,1);
}
void Voperation(int index,int semid)
{
struct sembuf sop;
sop.sem_num = index;
sop.sem_op = 1;
sop.sem_flg = 0;
semop(semid,&sop,1);
}
2、使用union semun初始化信号量:常与semctl()函数一起使用
union semun
{
int val;
}
3、生成共享内存:
int shmid;
char *shmaddr;
shmid = shmget(key,128,IPC_CREAT|0755);
4、宏定义读写两个信号量:
#define SEM_WRITE 1
#define SEM_READ 0
5、 使用semctl函数来控制读写两个信号量:
//init semaphore
union semum myun;
//init semaphore read
myun.val=0;
semctl(semid,SEM_READ,SETVAL,myun);
//init semaphore write
myun.val = 1;
semctl(semid,SEM_WRITE,SETVAL,myun);
6、生成父子进程,并分别实现父子进程的动作:
父进程实现从键盘输入,并执行写操作,读操作暂停:
子进程执行读操作,写操作暂停;
pid_t pid;
pid = fork();
if(pid == 0)//child process
{
while(1)
{
shmaddr = (char *)shmat(shmid,NULL,0);
Poperation(SEM_READ,semid);
printf("get share memory is %s\n",shmaddr);
Voperation(SEM_WRITE,semid);
}
}
else if(pid>0)//father process
{
while(1)
{
shmaddr =( char *)shmat(shmid,NULL,0);
Poperation(SEM_WRITE,semid);
printf("please input to share memory\n");
fgets(shmaddr,32,stdin);
Voperation(SEM_READ,semid);
}
}
完整代码:
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#define SEM_WRITE 1
#define SEM_READ 0
//ding yi xin hao lian he ti
// 必须手动定义 semun union,因为它不在标准头文件中定义
union semun {
int val; // 用于 SETVAL
struct semid_ds *buf; // 用于 IPC_STAT 和 IPC_SET
unsigned short *array; // 用于 GETALL 和 SETALL
struct seminfo *__buf; // 用于 IPC_INFO(可选,可以忽略)
};
void Poperation(int index,int semid)
{
struct sembuf sop;
sop.sem_num = index;
sop.sem_op = -1;
sop.sem_flg = 0;
semop(semid,&sop,1);
}
void Voperation(int index,int semid)
{
struct sembuf sop;
sop.sem_num = index;
sop.sem_op = 1;
sop.sem_flg = 0;
semop(semid,&sop,1);
}
void Voperation(int index,int semid)
{
struct sembuf sop;
sop.sem_num = index;
sop.sem_op = 1;
sop.sem_flg = 0;
semop(semid,&sop,1);
}
int main()
{
key_t key;
key = ftok(".",123);
if(key == -1)
{
perror("ftok");
return -1;
}
int semid;
int shmid;
char *shmaddr;
semid = semget(key,2,IPC_CREAT|0755);
if(semid<0)
{
perror("semget");
return -1;
}
shmid = shmget(key,128,IPC_CREAT|0755);
if(shmid<0)
{
perror("shmget");
return -2;
}
//init semaphore
union semun myun;
//init semaphore read
myun.val=0;
semctl(semid,SEM_READ,SETVAL,myun);
//init semaphore write
myun.val = 1;
semctl(semid,SEM_WRITE,SETVAL,myun);
pid_t pid;
pid = fork();
if(pid == 0)//child process
{
while(1)
{
shmaddr = (char *)shmat(shmid,NULL,0);
Poperation(SEM_READ,semid);
printf("get share memory is %s\n",shmaddr);
Voperation(SEM_WRITE,semid);
}
}
else if(pid>0)//father process
{
while(1)
{
shmaddr =( char *)shmat(shmid,NULL,0);
Poperation(SEM_WRITE,semid);
printf("please input to share memory\n");
fgets(shmaddr,32,stdin);
Voperation(SEM_READ,semid);
}
}
return 0;
}
结果:父子进程就会交替执行读写操作,并不会同时占用共享内存: