Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
通讯目的:
一个进程在管道的尾部写入数据,另一个进程从管道的头部读出数据。管道包括无名管道和有名管道两种,前者只能用于父进程和子进程之间的通信,后者可与用于运行与同一系统中的任意两个进程间的通信。
管道通信的特点:
管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。
特点:
原型:
创建无名管道:
#include
int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1
管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:
练习:父子进程使用管道通信,父写入字符串,子进程读出并,打印到屏幕。
思考:为甚么,程序中没有使用sleep函数,但依然能保证子进程运行时一定会读到数据呢??????(wait阻塞等待)
#include
#include
#include
#include
#include
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(void)
{
pid_t pid;
char buf[1024];
int fd[2];
char *p = "test for pipe\n";
if (pipe(fd) == -1)
sys_err("pipe");
pid = fork();
if (pid < 0)
sys_err("fork err");
else if (pid == 0) // 子进程读数据
{
//sleep(1);
close(fd[1]);
int len = read(fd[0], buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
close(fd[0]);
}
else
{
close(fd[0]);
write(fd[1], p, strlen(p)); // 父进程写数据
wait(NULL);
close(fd[1]);
}
return 0;
}
运行结果
管道的读写行为
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
总结:
① 读管道: 1. 管道中有数据,read返回实际读到的字节数。
2. 管道中无数据:
(1) 管道写端被全部关闭,read返回0 (好像读到文件结尾)
(2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu,fcntl函数可以更改非阻塞)
② 写管道: 1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
2. 管道读端没有全部关闭:
(1) 管道已满,write阻塞。
(2) 管道未满,write将数据写入,并返回实际写入的字节数。
练习1:使用管道实现父子进程间通信,完成:ls | wc –l。假定父进程实现ls,子进程实现wc。
ls命令正常会将结果集写出到stdout,但现在会写入管道的写端;wc –l 正常应该从stdin读取数据,但此时会从管道的读端读。
#include
#include
#include
int main(void)
{
pid_t pid;
int fd[2];
pipe(fd);
pid = fork();
if (pid == 0)
{
close(fd[1]); //子进程从管道中读数据,关闭写端
dup2(fd[0], STDIN_FILENO); // 标准输入重定向管道读端
execlp("wc", "wc", "-l", NULL); //wc命令默认从标准读入取数据
}
else
{
close(fd[0]); //父进程向管道中写数据,关闭读端
dup2(fd[1], STDOUT_FILENO); // 标准输出重定向管道写端
execlp("ls", "ls", NULL); //ls输出结果默认对应屏幕
}
return 0;
}
运行结果
程序执行,发现程序执行结束,shell还在阻塞等待用户输入。这是因为,shell → fork → ./pipe1, 程序pipe1的子进程将stdin重定向给管道,父进程执行的ls会将结果集通过管道写给子进程。若父进程在子进程打印wc的结果到屏幕之前被shell调用wait回收,shell就会先输出$提示符。
练习2:使用管道实现兄弟进程间通信。 兄:ls 弟: wc -l 父:等待回收子进程。要求,使用“循环创建N个子进程”模型创建兄弟进程,使用循环因子i标示。注意管道读写行为。
#include
#include
#include
int main(void)
{
pid_t pid;
int fd[2], i;
pipe(fd);
for (i = 0; i < 2; i++)
{
if((pid = fork()) == 0)
{
break;
}
}
if (i == 0)
{ //兄
close(fd[0]); //写,关闭读端
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
}
else if (i == 1)
{ //弟
close(fd[1]); //读,关闭写端
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
}
else
{
close(fd[0]);
close(fd[1]);
for(i = 0; i < 2; i++) //两个儿子wait两次
wait(NULL);
}
return 0;
}
运行结果
测试:是否允许,一个pipe有一个写端,多个读端呢?是否允许有一个读端多个写端呢?
#include
#include
#include
#include
#include
int main(void)
{
pid_t pid;
int fd[2], i, n;
char buf[1024];
int ret = pipe(fd);
if(ret == -1)
{
perror("pipe error");
exit(1);
}
for(i = 0; i < 2; i++)
{
if((pid = fork()) == 0)
break;
else if(pid == -1)
{
perror("pipe error");
exit(1);
}
}
if (i == 0)
{
close(fd[0]);
write(fd[1], "hello ", strlen("hello "));
}
else if(i == 1)
{
sleep(1);
close(fd[0]);
write(fd[1], "world\n", strlen("world\n"));
}
else
{
sleep(2)
close(fd[1]); //父进程关闭写端,留读端读取数据
n = read(fd[0], buf, 1024); //从管道中读数据
write(STDOUT_FILENO, buf, n);
for(i = 0; i < 2; i++) //两个儿子wait两次
wait(NULL);
}
return 0;
}
运行结果
inux@ubuntu64-vm:~/workdir/test$ ./pipe3
hello world
linux@ubuntu64-vm:~/workdir/test$
道缓冲区大小
管道的优劣
FIFO文件的特点:
原型:
创建有名管道:
当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:
例子
FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清除数据,并且“先进先出”。下面的例子演示了使用 FIFO 进行 IPC 的过程:
代码
fifo_w.c
#include
#include
#include
#include
#include
#include
#include
void sys_err(char *str)
{
perror(str);
exit(-1);
}
int main(void)
{
int fd, i;
char buf[4096];
fd = open("./myfifo", O_WRONLY);
if (fd < 0)
sys_err("open");
i = 0;
while (1)
{
memset(buf, 0x00, sizeof(buf));
sprintf(buf, "hello itcast %d\n", i++);
write(fd, buf, strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
fifo_r.c
#include
#include
#include
#include
#include
#include
#include
void sys_err(char *str)
{
perror(str);
exit(-1);
}
int main(void)
{
int fd, len;
char buf[4096];
fd = open("./myfifo", O_RDONLY);
if (fd < 0)
sys_err("open");
while (1)
{
len = read(fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
sleep(3); //多個读端时应增加睡眠秒数,放大效果.
}
close(fd);
return 0;
}
在Linux系统中,信号(signal)是一种事件通知的进程通讯方式。
原型:
向一个进程发送信号
#include
#include
// 成功返回0,失败返回-1
int kill(pid_t pid, int sig);
pid:pid>0,pid参数指向接收信号的进程;sig:指明我们要发送的信号。
处理信号:参考之前写的博客
https://blog.csdn.net/qq_22847457/article/details/89278451
https://blog.csdn.net/qq_22847457/article/details/89113559
例子:A、B进程利用信号通讯:A进程发送SIGINT信号给B进程;B进程设置SIGINT的处理方式,然后等待pause。
代码:
bprocess.c
#include
#include
void myfunc(int a)
{
printf("Process B received SIGINT\n");
}
void main()
{
signal(SIGINT, myfunc);
pause();
}
aprocess.c
#include
#include
#include
void main(int argc, char *argv[])
{
pid_t pid;
pid = atoi(argv[1]); // B进程进程号
kill(pid, SIGINT);
}
运行结果:
信号量本质上是一个计数器(不设置全局变量是因为进程间是相互独立的,而这不一定能看到,看到也不能保证++引用计数为原子操作),用于多进程对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。
信号量的工作原理
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
在信号量进行PV操作时都为原子操作(因为它需要保护临界资源)
注:原子操作:单指令的操作称为原子的,单条指令的执行是不会被打断的
二元信号量
二元信号量(Binary Semaphore)是最简单的一种锁(互斥锁),它只用两种状态:占用与非占用。所以它的引用计数为1。
进程如何获得共享资源
注:信号量通过同步与互斥保证访问资源的一致性
与信号量相关的函数
创建/打开信号量集合
int semget(key_t key,int nsems,int flags)//返回:成功返回信号集ID,出错返回-1
删除和初始化信号量
int semctl(int semid, int semnum, int cmd, ...);
如有需要第四个参数一般设置为union semnu arg;定义如下
union semun
{
int val; //使用的值
struct semid_ds *buf; //IPC_STAT、IPC_SET 使用的缓存区
unsigned short *arry; //GETALL,、SETALL 使用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区
};
改变信号量的值
int semop(int semid, struct sembuf *sops, size_t nops);
struct sembuf
{
short sem_num; //除非使用一组信号量,否则它为0
short sem_op; //信号量在一次操作中需要改变的数据,通常是两个数,
//一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
};
例子:利用信号量互斥,A、B同学往公告栏中写入数据。
代码:
studentA.c:
#include
#include
#include
#include
#include
#include
void main()
{
int fd = 0;
key_t key;
int semid;
struct sembuf sops;
// 1.创建键值
key = ftok("/home", 1);
/* 2.创建并打开信号量集合 */
semid= semget(key, 1, IPC_CREAT);
/* 3.打开公告栏 */
fd = open("./board.txt", O_RDWR | O_APPEND);
semctl(semid, 0, SETVAL, 1); // 设置初始值为1
/* 4.获取信号量 */
sops.sem_num = 0;
sops.sem_op = -1;
sops.sem_flg = SEM_UNDO;
semop(semid, &sops, 1);
/* 5.向公告板文件里写入“数学课” */
write(fd, "class math", 11);
/* 6.休息10秒钟 */
sleep(10);
/* 7.向公告板文件里写入“取消” */
write(fd, "is cancel", 11);
/* 8.释放信号量 */
sops.sem_num = 0;
sops.sem_op = +1;
semop(semid, &sops, 1);
/* 9.关闭公告板文件 */
close(fd);
}
studentB.c:
#include
#include
#include
#include
#include
#include
#include
void main()
{
int fd = 0;
key_t key;
int semid;
int ret;
struct sembuf sops;
/* 1.创造键值 */
key = ftok("/home", 1);
/* 2.打开信号量集合 */
semid= semget(key, 1, IPC_CREAT);
/* 3.打开公告板 */
fd = open("./board.txt", O_RDWR | O_APPEND);
ret = semctl(semid, 0, GETVAL);
printf("ret is %d\n",ret);
/* 4.获取信号量 */
sops.sem_num = 0;
sops.sem_op = -1;
semop(semid, &sops, 1);
/* 5.写入英语课考试 */
write(fd, "english exam ", 20);
/* 6.释放信号量 */
sops.sem_num = 0;
sops.sem_op = +1;
sops.sem_flg = SEM_UNDO;
semop(semid, &sops, 1);
/* 7.关闭公告板文件 */
close(fd);
}
运行结果:
可见,A同学写入class math后休息的10s时间内,B同学进程一直在等待,直到A同学写完,释放信号量,B同学才执行写入操作。
例2:利用信号量同步编程,解决生产者与消费者问题,生产者释放信号量,消费者获取信号量。
producor.c
#include
#include
#include
#include
#include
void main()
{
int fd;
key_t key;
int semid;
struct sembuf sops;
// 1.创建键值
key = ftok("/home", 2);
// 2.创建信号量集合
semid = semget(key, 1, IPC_CREAT);
// 3.设置信号量的值为0
semctl(semid, 0, SETVAL, 0);
// 4.创建产品文件
fd = open("./product.txt", O_RDWR | O_CREAT);
// 5.休息10s
sleep(10);
// 6.向产品文件里写入数据
write(fd, "the product is fished", 25);
// 7.释放信号量
sops.sem_num = 0;
sops.sem_op = +1;
sops.sem_flg = SEM_UNDO;
semop(semid, &sops, 1);
// 8.关闭产品文件
close(fd);
}
customer.c
#include
#include
#include
#include
#include
void main()
{
key_t key;
int semid;
struct sembuf sops;
int ret;
// 1.创建键值
key = ftok("/home", 2);
// 2.创建信号量集合
semid = semget(key, 1, IPC_CREAT);
// 3.获取信号量
sops.sem_num = 0;
sops.sem_op = -1;
sops.sem_flg = SEM_UNDO;
ret = semop(semid, &sops, 1);
// 4.打印信号量的值
printf("ret is %d!\n",ret);
// 5.取走产品文件
system("cp ./product.txt ./ship"); // system系统调用
}
共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
特别提醒:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。所以我们通常需要用其他的机制来同步对共享内存的访问,例如前面说到的信号量。
与信号量一样,在Linux中也提供了一组函数接口用于使用共享内存,而且使用共享共存的接口还与信号量的非常相似,而且比使用信号量的接口来得简单。它们声明在头文件 sys/shm.h中。
shmget函数
创建\获取共享内存,它的原型为:
int shmget(key_t key, size_t size, int shmflg);
成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1。
shmat函数
映射共享内存,第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。它的原型如下:
void *shmat(int shm_id, const void *shm_addr, int shmflg);
调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.
shmdt函数
该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:
int shmdt(const void *shmaddr);
参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1.
shmctl函数
与信号量的semctl函数一样,用来控制共享内存,它的原型如下:
int shmctl(int shm_id, int command, struct shmid_ds *buf);
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID:删除共享内存段
shmid_ds结构至少包括以下成员:
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
实例:A进程创建创建内存,并将其映射到自己的内存地址空间,A进程往该空间中写入数据;B进程也将该共享内存映射到自己的内存地址空间,读取其中的数据。A、B进程断开与该共享内存的连接,A进程删除该共享内存。
代码:
write.c
#include
#include
#include
#include
#include
#include
#include
#define TEXT_SZ 2048
struct shared_use_st
{
int written_by_you; // 标志
char some_text[TEXT_SZ];
};
int main()
{
int running = 1; // 循环标志
int shmid;
struct shared_use_st *shared_stuff;
char buffer[TEXT_SZ];
// 1.创建共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), IPC_CREAT | 0777); // 键值1234
if (shmid == -1)
{
printf("creat share memory fail!\n");
exit(EXIT_FAILURE);
}
// 2.映射共享内存
shared_stuff = (struct shared_use_st *)shmat(shmid, NULL, 0);
// 3.循环
while (running)
{
while (shared_stuff->written_by_you == 1)
{
sleep(1);
printf("wait read process!\n");
}
// 3.1获取用户输入
fgets(buffer, TEXT_SZ, stdin);
// 3.2将用户输入的字符串放入共享内存
strncpy(shared_stuff->some_text, buffer, TEXT_SZ);
shared_stuff->written_by_you = 1;
if (strncmp(buffer, "end", 3) == 0)
running = 0;
}
// 4.脱离共享内存
shmdt((const void *)shared_stuff);
return 1;
}
read.c
#include
#include
#include
#include
#include
#include
#include
#define TEXT_SZ 2048
struct shared_use_st
{
int written_by_you; // 标志
char some_text[TEXT_SZ];
};
int main()
{
int shmid;
int running = 1; // 循环标志
struct shared_use_st *shared_stuff;
// 1.获取共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), IPC_CREAT | 0777);
// 2.映射共享内存
shared_stuff = (struct shared_use_st *)shmat(shmid, NULL, 0);
shared_stuff->written_by_you = 0;
// 3.循环
while (running)
{
// 3.1打印共享内存
if (shared_stuff->written_by_you == 1)
{
printf("write process write: %s\n",shared_stuff->some_text);
shared_stuff->written_by_you = 0;
if (strncmp(shared_stuff->some_text, "end", 3)== 0)
running = 0;
}
}
// 4.脱离共享内存
shmdt((const void *)shared_stuff);
// 5.删除共享内存
shmctl(shmid, IPC_RMID, 0);
return 1;
}
运行结果:
消息队列,是消息的链接表,消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
特点:
Linux用宏MSGMAX和MSGMNB来限制一条消息的最大长度和一个队列的最大长度。
Linux提供了一系列消息队列的函数接口来让我们方便地使用它来实现进程间的通信。它的用法与其他两个System V PIC机制,即信号量和共享内存相似。
msgget函数
该函数用来创建和打开一个消息队列。它的原型为:
int msgget(key_t, key, int msgflg);
成功返回一个以key命名的消息队列的标识符(非零整数),失败时返回-1.
msgsnd函数
该函数用来把消息添加到消息队列中。它的原型为:
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
struct my_message
{
long int message_type;
/* The data you wish to transfer*/
};
如果调用成功,消息数据的一分副本将被放到消息队列中,并返回0,失败时返回-1.
msgrcv函数
该函数用来从一个消息队列获取消息,它的原型为
int msgrcv(int msgid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,然后删除消息队列中的对应消息。失败时返回-1.
msgctl函数
该函数用来控制消息队列,它与共享内存的shmctl函数相似,它的原型为:
int msgctl(int msgid, int command, struct msgid_ds *buf);
command是将要采取的动作,它可以取3个值,
buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。msgid_ds结构至少包括以下成员:
struct msgid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
成功时返回0,失败时返回-1.
实例:A进程负责从键盘获取用户输入字符串,将之构造成消息,并添加到消息队列里面去;B进程负责从消息队列中取出消息,然后打印消息里面的信息。
代码:
send.c
#include
#include
#include
#include
#include
struct msgt
{
long msgtype; // 消息类型
char msgtext[1024];
};
void main()
{
int msgid;
int msg_type;
char str[256];
struct msgt msgs;
// 1.创建消息队列
msgid = msgget((key_t)1024, IPC_CREAT | 0777); // 键值1024
// 2.循环
while (1)
{
printf("please input message type, 0 for quit!\n");
// 2.1获取消息类型
scanf("%d",&msg_type);
// 2.2如果用户输入的消息为0,退出该循环
if (msg_type == 0)
break;
// 2.3获取消息数据
printf("please input message content!\n");
scanf("%s",str);
msgs.msgtype = msg_type;
strcpy(msgs.msgtext,str);
//2.将消息加入消息队列
msgsnd(msgid, &msgs, sizeof(msgs.msgtext), 0);
}
// 3.删除消息队列
msgctl(msgid, IPC_RMID, 0);
}
receive.c
#include
#include
#include
#include
#include
struct msgt
{
long msgtype;
char msgtext[1024];
};
int msgid = 0;
void childprocess()
{
struct msgt msgs;
while(1)
{
// 1.接收消息队列
msgrcv(msgid, &msgs, sizeof(msgs.msgtext), 0, 0);
// 2.打印消息队列的数
printf("msg text: %s\n", msgs.msgtext);
}
}
void main()
{
int i;
int cpid;
// 1.打开消息队列
msgid = msgget((key_t)1024, IPC_EXCL);
// 2.创建3个子进程
for (i = 0; i < 3; i++)
{
cpid = fork();
if (cpid < 0)
{
printf("creat child process error!\n");
}
else if (cpid ==0)
{
childprocess();
}
}
}
五种通讯方式总结: