发展
1、早期通信方式
2、AT&T 的贝尔实验室,对 Unix 早期的进程间通信进行了改进和扩充,形成了“system V IPC”
(InterProcess Communication),其通信进程主要局限在单个计算机内。
3、BSD(加州大学伯克利分校的伯克利软件发布中心),跳过了只能在同一计算机通信的限制,
形成了基于套接字(socket)的进程间通信机制。
进程间通信方式
1、早期通信:
无名管道(pipe),有名管道(fifo),信号(sem)
将数据写入管道、从管道中读出数据。
2、system V IPC:
共享内存(share memory),信号灯集(semaphore),消息队列(message queue)
共享内存:被两个或多个进程共享,通信效率最高。
信号灯集:控制同步与互斥。
消息队列:根据类型不同,匹配不同的接收/发送方。
3、BSD:
套接字(socket)
进程间的早期通信
无名管道(pipe)
无名管道的特点
1、只能用于具有亲缘关系的进程之间通信;
2、半双工通信,具有固定的读端和写端;
单工: 只能单方面传输信息,(广播、电视……)
半双工: 可以双向传输信息,但同一时刻只能一个方向传输信息,(对讲机……)
全双工: 可以双向、同时传输信息,(电话……)
3、可以看作特殊的文件(但不是文件),对于无名管道的读写可以使用 文件IO函数 read、write;
4、基于文件描述符进行通信,
当一个无名管道建立,它会创建两个文件描述符,一个是固定的读端,一个是固定的写端。
函数
int pipe(int fd[2]);
功能:创建无名管道
参数:文件描述符 fd[0]:读端 fd[1]:写端
返回值:成功 0
失败 -1
注意事项
1、当管道中无数据时,读会阻塞
2、当管道中写满(64k)数据时,写阻塞;一旦有4k空间,写继续,直到写满为止
3、将写端关闭,读操作立即返回(管道中无数据时)
4、将读端关闭,继续写数据,会导致管道破裂
进程会收到内核发送过来的SIGPIPE信号(Broken pipe)。
练习:利用无名管道实现进程通信
// 父子进程实现通信,父进程循环从终端输入数据,子进程循环打印数据,当输入 quit 结束。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
char buf[64] = {};
int fd[2];
if (pipe(fd)){
perror("Failed: ");
return -1;
}
pid_t pid = fork();
if (pid < 0){
perror("Failed: ");
return -1;
} else if (pid > 0){
while (1){
scanf("%s", buf);
write(fd[1], buf, 64); // 这里也不能用 strlen(buf)
if (!strcmp(buf, "quit"))
break;
}
wait(NULL); // 阻塞等待子进程结束,回收子进程
} else {
while (1){
read(fd[0], buf, 64); // 此 buf 非彼 buf, 不能用 strlen(buf)
if (!strcmp(buf, "quit"))
break;
printf("%s\n", buf);
}
}
close(fd[0]);
close(fd[1]);
return 0;
}
运行结果如下:
有名管道(fifo)
有名管道的特点
1、有名管道可以使互不相关的两个进程互相通信;
2、有名管道可以 通过路径名来指出,并且在文件系统中可见,但内容存放在内存中;
3、通过 文件IO 来操作有名管道,半双工通信;
4、有名管道遵循 先进先出规则;
5、不支持 lseek() 操作。
函数
int mkfifo(const char *filename,mode_t mode);
功能:创健有名管道
参数:filename:有名管道文件名
mode:权限
返回值:成功:0
失败:-1,并设置errno号
注意对错误的处理方式:
如果错误是file exist时,注意加判断,如:if(errno == EEXIST) 或 if(errno == 17)
注意事项:
1、只写方式,写阻塞(open处),直到另一个进程将读打开
2、只读方式,读阻塞(open处),直到另一个进程将写打开
3、可读可写,若管道中无数据,读阻塞
练习:通过两个进程实现 cp功能
// Write_into_fifo.c
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if (mkfifo("./fifo", 0666)){
if (errno == EEXIST) // 或 errno == 17
printf("File exists!\n");
else {
perror("Failed ");
return -1;
}
}
int fd_f = open("./fifo", O_WRONLY); // 不要写 O_RDONLY !!!
int fd_txt = open(argv[1], O_RDONLY);
if (fd_f < 0 || fd_txt < 0){
perror("Failed ");
return -1;
}
char buf[64] = {};
while (1){
int num = read(fd_txt, buf, 64); // 从 A文件 读入 数组
if (num <= 0)
break;
write(fd_f, buf, num); // 从 数组 写入 有名管道
}
close(fd_f);
close(fd_txt);
return 0;
}
// Read_from_fifo.c
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if (mkfifo("./fifo", 0666)){
if (errno == EEXIST) // 或 errno == 17
printf("File exists!\n");
else {
perror("Failed ");
return -1;
}
}
int fd_f = open("./fifo", O_RDONLY); // 不要写 O_WRONLY !!!
int fd_txt = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd_f < 0 || fd_txt < 0){
perror("Failed ");
return -1;
}
char buf[64] = {};
while (1){
int num = read(fd_f, buf, 64); // 从 有名管道 读入数组
if (num <= 0)
break;
write(fd_txt, buf, num); // 从 数组 写入 B文件
}
close(fd_f);
close(fd_txt);
return 0;
}
input.c --> 读源文件,写到管道里
1)创建管道
2)打开源文件,管道文件
3)循环读源文件,写管道
4)关闭文件描述符
output.c --> 读管道,写到目标文件里面
1)创建管道
2)打开目标文件,管道文件
3)循环读管道文件,写目标文件
4)关闭文件描述符
练习:两进程间实现全双工
有名管道 & 无名管道
信号
概念
1)信号是在软件层次上对中断机制的一种模拟,是异步通信方式;
2)信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件;
3)如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
信号的响应方式
1、忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即 SIGKILL 及 SIGSTOP。
2、捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。pause() 结束的条件。
3、执行缺省操作:Linux 对每种信号都规定了默认操作 。
信号种类
函数接口
kill
int kill(pid_t pid, int sig);
功能:信号发送
参数:pid:指定进程
sig:要发送的信号
返回值:成功 0
失败 -1
raise
int raise(int sig);
功能:进程向自己发送信号
参数:sig:信号
返回值:成功 0
失败 -1
pause
int pause(void);
功能:用于将调用进程挂起,直到捕捉到信号为止。
alarm
unsigned int alarm(unsigned int seconds);
功能:在进程中设置一个定时器
参数:seconds:定时时间,单位为秒
返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
注意:一个进程只能有一个闹钟时间。如果在调用alarm时已设置过闹钟时间,则之前的闹钟时间被新值所代替
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int a = alarm(5); // 这里不是最后一个 alarm, 不等待
printf("%d\n", a); // a == 0, 第一次调用 alarm 返回值为 0
printf("Hola~ \n");
sleep(2); // 等待 2s
printf("%d\n", alarm(20)); // 再次调用 alarm 将返回上次所剩时间 == 5-2 == 3
// 最后一个 alarm, 等待
pause();
return 0;
}
运行结果如下:
signal
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数:signum:要处理的信号
handler:信号处理方式
SIG_IGN:忽略信号
SIG_DFL:执行默认操作
handler:捕捉信号 void handler(int sig){} //函数名可以自定义
返回值:成功:设置之前的信号处理方式
失败:-1
1、忽略信号
signal(信号, SIG_IGN);
有两个信号不能忽略:即 SIGKILL 及 SIGSTOP。
2、缺省操作
signal(信号, SIG_DFL);
3、捕捉操作
void handler(int sig){
/* blablabla… */
}void main(){
signal(信号, handler);
}
练习:司机和售票员问题
1)售票员捕捉 SIGINT(代表开车)信号,向司机发送 SIGUSR1 信号,司机打印(Let’s go)
2)售票员捕捉 SIGQUIT(代表停车)信号,向司机发送 SIGUSR2 信号,司机打印(Stop the bus)
3)司机捕捉 SIGTSTP(代表到达终点站)信号,向售票员发送 SIGUSR1 信号,
售票员打印(Please get off the bus)
4)司机等待售票员下车,之后司机再下车。
分析:由 4)可知,利用父子进程完成此练习 —> 售票员子进程,司机父进程
司机:
忽略:SIGINT、SIGQUIT
捕捉:SIGUSR1 —> (Let’s go)
SIGUSR2 —> (Stop the bus)
SIGTSTP —> 向售货员发送 SIGUSR1 —> 等待回收子进程
售票员:
忽略:SIGTSTP
捕捉:SIGINT —> 向司机发送 SIGUSR1
SIGQUIT —> 向司机发送 SIGUSR2
SIGUSR1 —> (Please get off the bus)—> 结束子进程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
pid_t pid, pid_f, pid_c;
void handler(int sig){
switch (sig)
{
case SIGINT:
kill(getppid(), SIGUSR1);
break;
case SIGQUIT:
kill(getppid(), SIGUSR2); // 子进程获取父进程的ID,向父进程传送信号
break;
case SIGUSR1:
if (getpid() == pid_f){
printf("\nLet's go!\n");
}
if (getpid() == pid_c){
printf("\nPlease get off the bus!\n");
exit(0);
}
break;
case SIGUSR2:
printf("\nStop the bus!\n");
break;
case SIGTSTP:
kill(pid, SIGUSR1); // 父进程获取子进程的ID,向子进程传送信号
break;
default:
break;
}
}
int main(int argc, char const *argv[])
{
pid = fork(); // 父进程内返回子进程的ID,子进程内返回0
if (pid < 0){
perror("Failed ");
return -1;
} else if (pid == 0){
pid_c = getpid();
signal(SIGTSTP, SIG_IGN);
signal(SIGINT, handler);
signal(SIGQUIT, handler);
signal(SIGUSR1, handler);
while(1) // 保证可以循环输入信号,不能写在 return 0 前面,
pause(); // 否则执行不到就退出程序
} else {
pid_f = getpid();
signal(SIGINT, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
signal(SIGUSR1, handler);
signal(SIGUSR2, handler);
signal(SIGTSTP, handler);
wait(NULL); // 等待回收子进程
exit(0); // 这里要退出父进程,后面的语句不再执行
}
return 0;
}
也可以将捕捉函数拆分成两个:
运行结果如下: