管道
匿名管道
pipe()函数的参数pipefd是一个数组,当在程序中使用pipe()创建管道时,程序可以通过传参的方式获取两个文件描述符,分别交给需要通信的两个进程,内核再将这两个进程中文件描述符对应file结构中的inode指向同一个临时的VFS索引结点,并使这个索引结点指向同一个物理页面。管道实现机制如图8-2所示。
案例1:使用pipe()实现父子进程间通信,要求父进程作为写端,子进程作为读端。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int fd[2]; //定义文件描述符数组
int ret = pipe(fd); //创建管道
if (ret == -1)
{
perror("pipe");
exit(1);
}
pid_t pid = fork();
if (pid > 0)
{
//父进程—写
close(fd[0]); //关闭读端
char *p = "hello,pipe\n";
write(fd[1], p, strlen(p) + 1); //写数据
close(fd[1]);
wait(NULL);
}
else if (pid == 0)
{
//子进程—读
close(fd[1]); //关闭写端
char buf[64] = { 0 };
ret = read(fd[0], buf, sizeof(buf)); //读数据
close(fd[0]);
write(STDOUT_FILENO, buf, ret); //将读到的数据写到标准输出
}
return 0;
}
有亲缘关系的进程,除父子外,还有兄弟进程等具备其他联系的进程。这些进程都依靠fork()创建,因此每个进程初始时都会有两个指向管道文件的文件描述符。实现这些进程间通信的实质是关闭多个进程中多余的文件描述符,只为待通信进程各自保留读端或写端。假设要实现兄弟进程间的通信,那么系统中进程文件描述符与管道的关系如图8-5所示,其中实线所示的箭头为编程中需要保留的文件描述符。
案例2:使用管道实现兄弟进程间通信,使兄弟进程实现命令“ls | wc-l”的功能。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if (ret == -1){
perror("pipe err");
exit(1);
}
int i;
pid_t pid, wpid;
for (i = 0; i < 2; i++){ //创建两个子进程
if ((pid = fork()) == 0)
break;
}
if (2 == i){ //父进程
close(fd[0]);
close(fd[1]);
wpid = wait(NULL);
printf("wait child 1 success,pid=%d\n", wpid);
pid = wait(NULL);
printf("wait child 2 success,pid=%d\n", pid);
}
else if (i == 0){ //子进程—写
close(fd[0]);
dup2(fd[1], STDOUT_FILENO); //将fd[1]所指文件内容定向到标准输出
execlp("ls", "ls", NULL);
}
else if (i == 1){ //子进程—读
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
}
return 0;
}
其中11为兄弟进程对ls | wc -l 命令的实现结果,在终端输人该命令,得到的结果与程序相同,可知案例成功实现。
管道是最简单的进程通信方式,但受自身数据传输机制的限制,使用管道时有以下几种情况需要注意:
- 管道采用半双工通信方式,只能进行单向数据传递。虽然多余的读写端口不一定会对程序造成影响,但为严谨起见,还是应使用close()函数关闭除通信端口之外的端口。
- 管道只能进行半双工通信,若要实现同时双向通信,需要为通信的进程创建两个 管道。
- 只有指向管道读端的文件描述符打开时,向管道中写人数据才有意义,否则写端的进程会收到内核传来的信号SIGPIPE,默认情况下该信号会导致进程终止。
- 若所有指向管道写端的文件描述符都被关闭后仍有进程从管道的读端读取数据,那 么管道中剩余的数据都被读取后,再次read会返回0。
- 若有指向管道写端的文件描述符未关闭,而管道写端的进程也没有向管道中写入数据,那么当进程从管道中读取数据且管道中剩余的数据都被读取时,再次read会阻 塞,直到写端向管道写人数据,阻塞才会解除。
- 若有指向管道读端的文件描述符没关闭,但读端进程没有从管道中读取数据,写端进程持续向管道中写入数据,那么管道缓存区写满时再次write会阻塞,直到读端将数据读出,阻塞才会解除。
- 管道中的数据以字节流的形式传输,这要求管道两端的进程事先约定好数据的格式。
popen()/pclose()
案例3:使用popen()函数与pclose()函数实现管道通信。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
FILE *r_fp, *w_fp;
char buf[100];
r_fp = popen("ls", "r"); //读取命令执行结果
w_fp = popen("wc -l", "w"); //将管道中的数据传递给进程
while (fgets(buf, sizeof(buf), r_fp) != NULL)
fputs(buf, w_fp);
pclose(r_fp);
pclose(w_fp);
return 0;
}
命名管道
案例4:使用FIFO实现没有亲缘关系进程间的通信。
读写文件
1) fifo_write.c ----写
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
if (argc < 2) //判断是否传入文件名
{
printf("./a.out fifoname\n");
exit(1);
}
int ret = access(argv[1], F_OK); //判断fifo文件是否存在
if (ret == -1) //若fifo不存在就创建fifo
{
int r = mkfifo(argv[1], 0664);
if (r == -1){ //判断文件是否创建成功
perror("mkfifo");
exit(1);
}
else{
printf("fifo creat success!\n");
}
}
int fd = open(argv[1], O_WRONLY); //以读写的方式打开文件
while (1){ //循环写入数据
char *p = "hello,world!";
write(fd, p, strlen(p) + 1);
sleep(1);
}
close(fd);
return 0;
}
- fifo_read.c---- 读
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
if (argc < 2) //判断是否传入文件名
{
printf("./a.out fifoname\n");
exit(1);
}
int ret = access(argv[1], F_OK); //判断文件是否存在
if (ret == -1) //若文件不存在则创建文件
{
int r = mkfifo(argv[1], 0664);
if (r == -1){
perror("mkfifo");
exit(1);
}
else{
printf("fifo creat success!\n");
}
}
int fd = open(argv[1], O_RDONLY); //打开文件
if (fd == -1){
perror("open");
exit(1);
}
while (1){ //不断读取fifo中的数据并打印
char buf[1024] = { 0 };
read(fd, buf, sizeof(buf));
printf("buf=%s\n", buf);
}
close(fd); //关闭文件
return 0;
}
编译以上二段代码,假设传入的文件名为myfifo,分别执行程序,执行read功能的程序在终端打印的信息如下;
消息队列
消息队列的步骤
msgget()
msgsnd()
msgrev()
msgctl()
案列5:使用消息队列实现不同进程间的通信
一个发送端 一个接收端
- msgsend.c-------发送端
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <string.h>
#define MAX_TEXT 512
//消息结构体
struct my_msg_st{
long int my_msg_type; //消息类型
char anytext[MAX_TEXT]; //消息数据
};
int main()
{
int idx = 1;
int msgid;
struct my_msg_st data;
char buf[BUFSIZ]; //设置缓存变量
msgid = msgget((key_t)1000, 0664 | IPC_CREAT);//创建消息队列
if (msgid == -1){
perror("msgget err");
exit(-1);
}
while (idx < 5){ //发送消息
printf("enter some text:");
fgets(buf, BUFSIZ, stdin);
data.my_msg_type = rand() % 3 + 1; //随机获取消息类型
strcpy(data.anytext, buf);
//发送消息
if (msgsnd(msgid, (void*)&data, sizeof(data), 0) == -1){
perror("msgsnd err");
exit(-1);
}
idx++;
}
return 0;
}
- msgrcv.c ------接收端
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#define MAX_TEXT 512
struct my_msg_st{
long int my_msg_type;
char anytext[MAX_TEXT];
};
int main()
{
int idx = 1;
int msgid;
struct my_msg_st data;
long int msg_to_rcv = 0;
//rcv msg
msgid = msgget((key_t)1000, 0664 | IPC_CREAT);//获取消息队列
if (msgid == -1){
perror("msgget err");
exit(-1);
}
while (idx < 5){
//接收消息
if (msgrcv(msgid, (void*)&data, BUFSIZ, msg_to_rcv, 0) == -1){
perror("msgrcv err");
exit(-1);
}
//打印消息
printf("msg type:%ld\n", data.my_msg_type);
printf("msg content is:%s", data.anytext);
idx++;
}
//删除消息队列
if (msgctl(msgid, IPC_RMID, 0) == -1){
perror("msgctl err");
exit(-1);
}
exit(0);
}
msgsend.c第16行代码中的BUFSIZ为Linux系统定义的宏,定义在stdio.h中,表示默认的缓冲大小。程序msgsend.c作为消息发送方,向创建的消息队列中发送消息;程序msgrcv.c作为消息接收方,从消息队列中读取数据。编译以上两段代码,分别在不同的终端执行,当进程msgsend.c有消息输人时,进程msgrcv.c所在的终端会将消息从消息队列中读出。代码中设置发送消息的进程发送4条消息,执行程序后根据提示在终端输入如下4条信息,信息输人完毕后进程终止;
对多个进程来说,要通过消息队列机制实现进程间通信,必须能与相同消息队列进行关联,键值(key)就是实现进程与消息队列关联的关键。当在进程中调用msgget()函数创建消息队列时,传入的key值会被保存到内核中,与msgget()函数创建的消息队列一一对应;若进程中调用msgget()函数获取已存在的消息队列,只需要向msgget()函数中传入键值,就能获取内核中与键值对应的消息队列。也就是说,键值是消息队列在内存级别的唯一标识。
对单个进程来说,可能需要与多个进程间的通信,因此会和多个消息队列关联。当多次调用msgget()函数与多个消息队列进行关联时,每个msgget()函数都会返回一个非负整数,这个非负整数就是进程对消息队列的标识。标识符是消息队列在进程级别的唯一标识。除消息队列外,其他SystemVIPC类型(信号量、共享内存)的通信方式也使用与消息队列类似的编程接口,信号量通信与共享内存通信中用到的键值和标识符与消息队列中键值和标识符的功能类似。
信号量
说明: 系统中信号量的数量是有限制的,其极限值由宏SEMMSL设定
semget()
semctl()
semop()
案例6: 使用信号量实现父子进程同步,防止父子进程抢夺CPU
#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h>
//自定义共用体
union semu{
int val;
struct semid_ds* buf;
unsigned short* array;
struct seminfo* _buf;
};
static int sem_id;
//设置信号量值
static int set_semvalue()
{
union semu sem_union;
sem_union.val = 1;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
return 0;
return 1;
}
//p操作,获取信号量
static int semaphore_p()
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1){
perror("sem_p err");
return 0;
}
return 1;
}
//V操作,释放信号量
static int semaphore_v()
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1){
perror("sem_v err");
return 0;
}
return 1;
}
//删除信号量
static void del_semvalue()
{
union semu sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
perror("del err");
}
int main()
{
int i;
pid_t pid;
char ch = 'C';
sem_id = semget((key_t)1000, 1, 0664 | IPC_CREAT);//创建信号量
if (sem_id == -1){
perror("sem_c err");
exit(-1);
}
if (!set_semvalue()){ //设置信号量值
perror("init err");
exit(-1);
}
pid = fork(); //创建子进程
if (pid == -1){ //若创建失败
del_semvalue(); //删除信号量
exit(-1);
}
else if (pid == 0) //设置子进程打印的字符
ch = 'Z';
else //设置父进程打印的字符
ch = 'C';
srand((unsigned int)getpid()); //设置随机数种子
for (i = 0; i < 8; i++) //循环打印字符
{
semaphore_p(); //获取信号量
printf("%c", ch);
fflush(stdout); //将字符打印到屏幕
sleep(rand() % 4); //沉睡
printf("%c", ch);
fflush(stdout); //再次打印到屏幕
sleep(1);
semaphore_v(); //释放信号量
}
if (pid > 0){
wait(NULL); //回收子进程
del_semvalue(); //删除信号量
}
printf("\nprocess %d finished.\n", getpid());
return 0;
}
ftok()函数
例如,当前目录的inode值为65538,转换为十六进制为0x01002;
指定的proj_id值为24,转换为十六进制为0x18,那么ftok()返回的key值则为0x18010002。
共享内存
下面是Linux内核提供了一些系统调用,用于实现共享内存的申请、管理与释放,这些函数分别为:shmget、shmat、shmdt和shmctl
shmget()
shmat()
shmdt()
shmctl()
说明:
需要注意的是,共享内存与消息队列以及信号量相同,在使用完毕后都应该进行释放。另外,当调用fork函数创建子进程时,子进程会继承父进程已绑定的共享内存;当调用exec函数更改子进程功能以及调用exit函数时,子进程中都会解除与共享内存的映射关系,因此在必要时仍应使用shmc函数对共享内存进行删除。
案例7:创建两个进程,使用共享内存机制实现这两个进程间的通信
shm_ r.c.
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct{
char name[8];
int age;
} Stu;
int main()
{
int shm_id, i;
key_t key;
Stu *smap;
struct shmid_ds buf;
key = ftok("/", 0); //获取关键字
if (key == -1)
{
perror("ftok error");
return -1;
}
printf("key=%d\n", key);
shm_id = shmget(key, 0, 0); //创建共享内存
if (shm_id == -1)
{
perror("shmget error");
return -1;
}
printf("shm_id=%d\n", shm_id);
smap = (Stu*)shmat(shm_id, NULL, 0); //将进程与共享内存绑定
for (i = 0; i < 3; i++) //读数据
{
printf("name:%s\n", (*(smap + i)).name);
printf("age :%d\n", (*(smap + i)).age);
}
if (shmdt(smap) == -1) //解除绑定
{
perror("detach error");
return -1;
}
shmctl(shm_id, IPC_RMID, &buf); //删除共享内存
return 0;
}
shm_ w.c.
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#define SEGSIZE 4096 //定义共享内存容量
typedef struct{ //读写数据结构体
char name[8];
int age;
} Stu;
int main()
{
int shm_id, i;
key_t key;
char name[8];
Stu *smap;
key = ftok("/", 0); //获取关键字
if (key == -1)
{
perror("ftok error");
return -1;
}
printf("key=%d\n", key);
//创建共享内存
shm_id = shmget(key, SEGSIZE, IPC_CREAT | IPC_EXCL | 0664);
if (shm_id == -1)
{
perror("create shared memory error\n");
return -1;
}
printf("shm_id=%d\n", shm_id);
smap = (Stu*)shmat(shm_id, NULL, 0); //将进程与共享内存绑定
memset(name, 0x00, sizeof(name));
strcpy(name, "Jhon");
name[4] = '0';
for (i = 0; i < 3; i++) //写数据
{
name[4] += 1;
strncpy((smap + i)->name, name, 5);
(smap + i)->age = 20 + i;
}
if (shmdt(smap) == -1) //解除绑定
{
perror("detach error");
return -1;
}
return 0;
}
struct ipc_ perm结构体
本章主要讲解Linux系统中进程间的通信机制,包括管道通信和SystemVIPC,其中管道通信分为匿名管道通信和命名管道通信;SystemVIPC分为消息队列通信、信号量通信与共享内存通信。了解Linux进程间的通信方式是学习Linux编程的基础,也是学习和实现复杂编程的基石。读者应尽力理解本章内容,掌握其中的接口函数,并且灵活运用本章知识实现Linux编程中的进程通信。