进程间的通信(IPC)方式
进程间的通信(IPC)方式,总归起来主要有如下这些:
1,无名管道(PIPE)和有名管道(FIFO)。
2,信号(signal)。
3,system V-IPC 之共享内存。
4,system V-IPC 之消息队列。
5,system V-IPC 之信号量。
6,套接字。
1、管道
1.1、无名管道
1.2、无名管道特征
PIPE 的特征:
1,没有名字,因此无法使用 open( )。
2,只能用于亲缘进程间(比如父子进程、兄弟进程、祖孙进程……)通信。
3,半双工工作方式:读写端分开。
4,写入操作不具有原子性,因此只能用于一对一的简单通信情形。
5,不能使用 lseek( )来定位。
1.3、需要注意的是
无名管道的读写端是固定的,读端(fd[0])只能进行读操作,写端(fd[1])只能进行写操作
1.4、读写数据的时候
(1)写数据的进程已经死了,管道中依然会存放写入的数据
(2)读操作读数据的时候,如果发现管道里面没有数据,会产生阻塞,一直等待管道中有数据
(3)管道中的数据一旦读完,管道就空了,不能在没有第二次写入数据之前,连续多次读取,如果管道中的数据
没有读取完,是可以直接进行第二次读取。
示例代码1
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <error.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
int main(int argc, char const *argv[])
{
//创建无名管道
int fd[2];
int ret = pipe(fd);
if (ret == -1)
{
perror("pipe");
return -1;
}
printf("无名管道创建成功\n");
//fd[0]:读端
//fd[1]:写端
int s;
//创建亲缘进程(父子进程)
pid_t x = fork();
char buf[100] = {0};
char w_buf[100] = {0};
if (x == 0)
{
printf("子进程开始运行\n");
read(fd[0], buf, sizeof(buf));
printf("buf = %s\n", buf);
printf("子进程结束运行\n");
}
else
if (x > 0)
{
printf("父进程开始运行\n");
scanf("%s", w_buf);
write(fd[1], w_buf, strlen(w_buf));
wait(&s);
printf("父进程结束运行\n");
}
else
{
perror("fork");
return -1;
}
return 0;
}
示例代码2:
通过无名管道实现父子进程的通信,父进程可以给子进程发,子进程也可以给父进程发。接收信息并打印,比如发送quit的时候,两个进程都结束。
#include <stdio.h>
#include <sys/types.h>
#include <error.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
int main(int argc, char const *argv[])
{
//创建无名管道
int fd[2];
int ret = pipe(fd);
if (ret == -1)
{
perror("pipe");
return -1;
}
printf("无名管道创建成功\n");
//fd[0]:读端
//fd[1]:写端
int s;
//创建亲缘进程(父子进程)
pid_t x = fork();
char buf[100] = {0};
char w_buf[100] = {0};
char buf1[100] = {0};
char w_buf1[100] = {0};
if (x == 0)
{
printf("子进程开始运行\n");
while(1)
{
bzero(buf, sizeof(buf));
read(fd[0], buf, sizeof(buf));
printf("收到父进程消息: %s\n", buf);
if (strcmp(buf, "quit") == 0)
{
return 0;
}
scanf("%s", w_buf1);
write(fd[1], w_buf1, strlen(w_buf1));
if (strcmp(w_buf1, "quit") == 0)
{
return 0;
}
sleep(1);
}
printf("子进程结束运行\n");
}
else
if (x > 0)
{
printf("父进程开始运行\n");
while(1)
{
scanf("%s", w_buf);
write(fd[1], w_buf, strlen(w_buf));
if (strcmp(w_buf, "quit") == 0)
{
return 0;
}
sleep(1);
bzero(buf1, sizeof(buf1));
read(fd[0], buf1, sizeof(buf1));
printf("收到子进程消息: %s\n", buf1);
if (strcmp(buf1, "quit") == 0)
{
return 0;
}
}
wait(&s);
printf("父进程结束运行\n");
}
else
{
perror("fork");
return -1;
}
return 0;
}
1.2、有名管道
有名管道 FIFO 的特征:
1,有名字,存储于普通文件系统之中。
2,任何具有相应权限的进程都可以使用 open( )来获取 FIFO 的文件描述符。
3,跟普通文件一样:使用统一的 read( )/write( )来读写。
4,跟普通文件不同:不能使用 lseek( )来定位,原因同 PIPE。
5,具有写入原子性,支持多写者同时进行写操作而数据不会互相践踏。
6,First In First Out,最先被写入 FIFO 的数据,最先被读出来。
读写阻塞的情况:
注意:管道文件(包括PIPE,FIFO,SOKET)不可以只有读端或者只有写端的情况下被打开
示例代码
通过有名管道实现两个不同的进程进行通信,输入quit结束两个进程。
Jack rose
应用:
//jack
//写端
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <error.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FIFO_PATHNAME "/tmp/myfifo"
int main(int argc, char const *argv[])
{
//创建有名管道(判断是否有这个文件)
if(access(FIFO_PATHNAME, F_OK)) //判断这个文件是否存在
{
int ret = mkfifo(FIFO_PATHNAME, 0644);
if (ret == -1)
{
perror("mkfifo");
return -1;
}
}
//打开有名管道
int fd = open(FIFO_PATHNAME, O_RDWR);
if (fd == -1)
{
perror("open");
return -1;
}
printf("有名管道打开成功\n");
char w_buf[1024] = {0};
char r_buf[1024] = {0};
while(1)
{
bzero(r_buf, sizeof(r_buf));
bzero(w_buf, sizeof(w_buf));
//写数据
scanf("%[^\n]", w_buf);
write(fd, w_buf, strlen(w_buf));
if (strcmp(w_buf, "quit") == 0)
{
return 0;
}
usleep(10);
read(fd, r_buf, sizeof(r_buf));
printf("rose:%s\n", r_buf);
if (strcmp(r_buf, "quit") == 0)
{
return 0;
}
}
return 0;
}
//rose
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <error.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
#include <fcntl.h>
#define FIFO_PATHNAME "/tmp/myfifo"
int main(int argc, char const *argv[])
{
//创建有名管道(判断是否有这个文件)
if(access(FIFO_PATHNAME, F_OK)) //判断这个文件是否存在
{
int ret = mkfifo(FIFO_PATHNAME, 0644);
if (ret == -1)
{
perror("mkfifo");
return -1;
}
}
//打开有名管道
int fd = open(FIFO_PATHNAME, O_RDWR);
if (fd == -1)
{
perror("open");
return -1;
}
printf("有名管道打开成功\n");
char r_buf[1024];
char w_buf[1024];
while(1)
{
//读数据
bzero(r_buf, sizeof(r_buf));
bzero(w_buf, sizeof(w_buf));
read(fd, r_buf, sizeof(r_buf));
if (strcmp(r_buf, "quit") == 0)
{
return 0;
}
printf("jack:%s\n", r_buf);
scanf("%[^\n]", w_buf);
write(fd, w_buf, strlen(w_buf));
if (strcmp(w_buf, "quit") == 0)
{
return 0;
}
usleep(10);
}
return 0;
}
2、信号
信号是一种比较特别的 IPC,一种异步通信机制
信号的生命周期:
进程产生信号,信号注册,信号响应和处理,信号注销
信号的响应方式:
(1)忽略信号
(2)捕捉响应信号函数
(3)执行缺省动作
特殊的信号:
信号 SIGKILL (-9) 和 SIGSTOP (-19) 是两个特殊的信号,他们不能被忽略、阻塞或捕捉,只能按缺省动作(默认)来响应。换句话说,除了这两个信号之外的其他信号,接收信号的目标进程按照如下顺序来做出反应:
A) 如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为
止。否则进入 B。
B) 如果该信号被捕捉,那么进一步判断捕捉的类型:
B1) 如果设置了响应函数,那么执行该响应函数。
B2) 如果设置为忽略,那么直接丢弃该信号。否则进入 C。
C) 执行该信号的缺省动作。
信号分类
查看系统的信号:kill –l
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cEXvQm3O-1630930236196)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f55ca06f47bf421eb9b45f5ff38df40c~tplv-k3u1fbpfcp-watermark.image)]
不可靠信号、非实时信号 前31个信号
特点:
1,非实时信号不排队,信号的响应会相互嵌套。
2,如果目标进程没有及时响应非实时信号,那么随后到达的该信号将会被丢弃。
3,每一个非实时信号都对应一个系统事件,当这个事件发生时,将产生这个信号。
4,如果进程的挂起信号中含有实时和非实时信号,那么进程优先响应实时信号并且会从大到小依此响应,而非实
时信号没有固定的次序。
可靠信号、实时信号 后31个信号
1,实时信号的响应次序按接收顺序排队,不嵌套。
2,即使相同的实时信号被同时发送多次,也不会被丢弃,而会依次挨个响应。
3,实时信号没有特殊的系统事件与之对应。
信号相关函数:
信号发送
信号捕捉
//信号响应函数
void func(int sig)
{
printf(“信号响应函数\n”);
printf(“sig = %d\n”, sig);
}
Sig为发送过来的信号的值
即kill发送 signal接收
示例代码
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <error.h>
#include <unistd.h>
#include <sys/wait.h>
//信号响应函数
void func(int sig)
{
printf("信号响应函数\n");
printf("sig = %d\n", sig);
}
int main(int argc, char const *argv[])
{
pid_t x = fork();
int i = 0;
int s;
if (x > 0) //父进程
{
sleep(5);
kill(x, 2);
wait(&s);
}
else
if (x == 0) //子进程
{
// signal(2, SIG_IGN); //忽略
// signal(2, SIG_DFL); //执行缺省动作
signal(2, func); //响应响应信号函数
while(1)
{
printf("i = %d\n", i++);
sleep(1);
if (i == 10)
{
break;
}
}
}
else
{
perror("fork");
}
return 0;
}
自己给自己发送:
示例代码
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <error.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
printf("本身ID:%d\n", getpid());
sleep(3);
raise(2);
printf("11111\n");
return 0;
}
将本进程挂起:
示例代码
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <error.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
printf("本身ID:%d\n", getpid());
int ret = pause();
printf("ret=%d\n", ret);
return 0;
}
信号集操作函数簇
阻塞或解除一个或多个信号
示例代码
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <error.h>
#include <unistd.h>
#include <sys/wait.h>
//信号响应函数
void func(int sig)
{
printf("捕捉的信号 %d\n", sig);
}
int main(int argc, char const *argv[])
{
sleep(1);
pid_t x = fork();
if (x > 0) //父进程
{
sleep(1);
//将所有的信号进行发送
printf("发送信号\n");
for (int i = 1; i < 64; ++i)
{
if (i==9 || i==19 || i==32 || i==33)
{
continue;
}
kill(x, i);
}
printf("所有信号发送完毕\n");
}
else
if (x == 0) //子进程
{
//将所有的信号先注册一个信号响应函数
for (int i = 1; i < 64; ++i)
{
if (i==9 || i==19 || i==32 || i==33)
{
continue;
}
signal(i, func);
}
//将这些信号添加到信号集里面
sigset_t mysig_set;
//清空信号集
sigemptyset(&mysig_set);
//将信号添加到信号集里面
sigfillset(&mysig_set);
//将所有的信号阻塞,在原有的基础上添加
sigprocmask(SIG_BLOCK, &mysig_set, NULL);
sleep(5);
//在解除所有阻塞的信号
sigprocmask(SIG_UNBLOCK, &mysig_set, NULL);
printf("子进程退出\n");
}
else
{
perror("fork");
}
return 0;
}
实现日志系统,5个客户端,往一个服务器发送日志信息,每个客户端发送日志信息的时间不一致,将获取到的日志信息,存放到TXT文本文件里面,发送格式:
client1:时间【2021/9/1 xx:xx:xx】
client2:
client3:
client4:
client5:
发送信号带数据
接收信号跟数据
扩展的响应函数接口如下:
void (*sa_sigaction)(int, siginfo_t *, void *);
该函数的参数列表详情:
第一个参数:int 型,就是触发该函数的信号。
第二个参数:siginfo_t 型指针,指向如下结构体:
siginfo_t
要去判断是否是sigqueue信号发送;
就通过 sinfo->si_code == SI_QUEUE
匹配成功,可以拿到里面的数据:sinfo->si_int
Sigqueue跟kill的区别:
Sigqueue:一个带数据
Kill:一个不带数据
Signal跟sigaction的区别:
Signal:一个不能接收数据,一个使用标准响应函数
Sigaction: 一个能接收数据,使用扩展响应函数,函数接口如下 void (*sa_sigaction)(int, siginfo_t *, void *);
信号内核数据结构
System IPC对象
多个进程通过相同的键值来进行通信
键值获取:
函数需要注意的几点:
1,如果两个参数相同,那么产生的 key 值也相同。
2,第一个参数一般取进程所在的目录,因为在一个项目中需要通信的几个进程通常会出现在同一个目录当中。
3,如果同一个目录中的进程需要超过 1 个 IPC 对象,可以通过第二个参数来标识。
4,系统中只有一套 key 标识,也就是说,不同类型的 IPC 对象也不能重复
可以使用以下命令来查看或删除当前系统中的 IPC 对象:
查看消息队列:ipcs -q
查看共享内存:ipcs -m
查看信号量:ipcs -s 功能 获取一个当前未用的 IPC 的 key
查看所有的 IPC 对象:ipcs -a
删除指定的消息队列:ipcrm -q MSG_ID 或者 ipcrm -Q msg_key
删除指定的共享内存:ipcrm -m SHM_ID 或者 ipcrm -M shm_key
删除指定的信号量:ipcrm -s SEM_ID 或者 ipcrm -S sem_key
消息队列(MSG)
消息队列提供一种带有数据标识的特殊管道,使得每一段被写入的数据都变成带标识的
消息,读取该段消息的进程只要指定这个标识就可以正确地读取,而不会受到其他消息的干扰
消息队列的使用方法一般是:
1,发送者:
A) 获取消息队列的 ID
B) 将数据放入一个附带有标识的特殊的结构体,发送给消息队列。
2,接收者:
A) 获取消息队列的 ID
B) 将指定标识的消息读出。
获取消息队列ID
发送、接收数据
关闭消息队列
示例代码
创建三个进程,小红,小明和小亮,小明跟小亮分别对小红说I like you,小红收到他们两个的信息,小红则对小明说I like you too,对小亮说,sorry,you are a goodboy。 使用消息队列完成。
共享内存
使用共享内存的一般步骤是:
1,获取共享内存对象的 ID
2,将共享内存映射至本进程虚拟内存空间的某个区域
3,当不再使用时,解除映射关系
4,当没有进程再需要这块共享内存时,删除它。
共享内存ID获取
共享内存进行映射
删除共享内存
信号量
1,多个进程或线程有可能同时访问的资源(变量、链表、文件等等)称为共享资源,也叫临界资源(critical resources)。
2,访问这些资源的代码称为临界代码,这些代码区域称为临界区(critical zone)。
3,程序进入临界区之前必须要对资源进行申请,这个动作被称为 P 操作,这就像你要把车开进停车场之前,先要向保安申请一张
停车卡一样,P 操作就是申请资源,如果申请成功,资源数将会减少。如果申请失败,要不在门口等,要不走人。
4,程序离开临界区之后必须要释放相应的资源,这个动作被称为 V 操作,这就像你把车开出停车场之后,要将停车卡归还给保安一样,
V 操作就是释放资源,释放资源就是让资源数增加。
解决资源竞态的问题
系统信号量跟有名管道,具有相同一个写入原子性的特征
信号量ID获取
初始化信号量
对信号量进行P/V操作
有名信号量:
(1)名字:“/myname” 注意:名字必须以/开头
(2)使用有名信号量需要用到pthread库(线程库) 如何使用这个信号量?
1、通过sem_open(),打开信号量
2、直接通过sem_wait(),sem_post()进行P、V操作
3、通过sem_close()关闭信号量
4、通过sem_unlink()删除信号量,释放资源
Sem_open()
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,
mode_t mode, unsigned int value);
通过sem_wait进行P操作(释放、归还)
#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
进行V操作sem_post()进行V操作
#include <semaphore.h>
int sem_post(sem_t *sem);
关闭信号量
#include <semaphore.h>
int sem_close(sem_t *sem);
删除信号量
#include <semaphore.h>
int sem_unlink(const char *name);
编译:
Gcc name.c –o name -lpthread
|