一、进程间通信方式介绍(了解)
1、进程间通信介绍
- 早期UNIX进程间通信方式;
无名管道(pipe)
有名管道(fifo)
信号(signal)
System V IPC(进程间通信缩写) ----- 三种:
共享内存(share memory)
消息队列(message queue)
信号灯集(semaphore set)
套接字(socket)
二、无名管道
1、无名管道特点(理解)
- 无名管道的数据是存放在内存中的,无名管道中的数据一旦读走,就不存在了;
- 只能用于有亲缘关系(父子进程,兄弟进程,祖孙进程)的进程间;因为 无名管道创建好后虽然返回两个文件描述符,但其在文件系统中是不可见的,他仅在内存中存在,并且无名管道是某一个进程创建的,其他的进程想要使用该管道,只能通过继承的方式得到该管道进行使用;
- 无名管道是单工的通信模式,具有固定的读端和写端;实现读写双向的话需要用两个无名管道;
- 父进程创建无名管道,子进程继承无名管道;
注:无名管道首先在内核中被创建,管道的一端固定为写,一端固定为读;
- 使用两个无名通道 可以 实现 两个进程 间的双向通信;
2、无名管道创建(熟练)
- 无名管道创建 — pipe
#include <unistd.h>
int pipe(int pfd[2]);
— 返回时返回0,失败时返回EOF;
— pfd包含两个元素的整型数组,用来保存文件描述符;
— 其中 pfd[0] 用于读管道;pfd[1]用于写管道;
示例:子进程1 和子进程2 分别往管道中写入字符串;父进程读管道内容并打印;
- 一定要写创建无名管道,在创建子进程;因为子进程要继承父进程的无名管道;
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <string.h>
int main(int argc, const char *argv[])
{
int pfd[2];
char buf[32];
pid_t pid1,pid2;
if(pipe(pfd)<0)
{
perror("pipe");
exit(-1);
}
if((pid1 = fork())<0)
{
perror("fork");
exit(-1);
}else if(pid1 == 0)//子进程1
{
strcpy(buf,"I'm process 1");
write(pfd[1],buf,32);
exit(0);
}else //父进程
{
if((pid2 = fork()) < 0)
{
perror("fork");
exit(-1);
}else if(pid2 == 0) //子进程2
{
sleep(1);
strcpy(buf,"I'm process 2");
write(pfd[1],buf,32);
}else //父进程
{
wait(NULL);//回收子进程
read(pfd[0],buf,32);//从管道读端读出 数据
printf("%s \n",buf);//打印 读出来的数据
wait(NULL);
read(pfd[0],buf,32);
printf("%s \n",buf);
}
}
return 0;
}
- 加一个睡眠,有意识的 让子进程1先去写管道,1秒后子进程2去写管道;
- wait 等待回收子进程;
- 无名管道与普通文件是不一样的;无名管道不会支持定位操作;
3、无名管道 读特性 (熟练)
- 读无名管道
- 写端存在:创建无名管道后会返回两个文件描述符;一个读一个写;
- 当写端存在时,读管道会出现那些情况:
- 有数据:read返回实际读取的 字节数;
- 无数据:进程阻塞;(读阻塞)
- 当写端不存在时
- 有数据 read返回实际读取的字节数;
- 无数据 read返回0;
- 当写端存在时,读管道会出现那些情况:
- 写端存在:创建无名管道后会返回两个文件描述符;一个读一个写;
4、无名管道 写特性 (熟练)
- 写无名管道
- 读端存在
- 当读端存在时,写管道会出现哪些情况:
- 有空间 write会返回实际写入的数据;
- 空间不足
- 空间还有部分,都不足与写一次:有多少空间先写多少空间,剩下的数据在管道空余后再写,此时进程会阻塞;
- 空间一点部分都没有:直接阻塞;
- 当读端存在时,写管道会出现哪些情况:
- 读端存在
5、无名管道 ---- 思考
- 如何获取无名管道的大小?
- 循环写入管道,直到阻塞
- 统计循环次数;
在Linux中,无名管道的缺省 大小为 64K Byte.
- 无名管道读端不存在时,称为管道断裂。对于没有读端的无名管道,系统不允许其写入,因为该无名管道中的数据不会被读走;
- 如何验证管道断裂(进程被信号结束)?
子进程写管道
父进程回收
小结:无名管道 与 有名管道的 特性都是一样的;
二、有名管道
无名管道的缺点:只能用于有亲属之间的进程。
使用时是单工的,只能实现一端读一端写的方式;
1、有名管道特点(了解)
- 有名管道创建后在系统中会看到一个实际的文件(Linux系统七的文件之一(P)),有名称,有路径,对用一个实际的管道文件;无名管道创建后存在于内存中,没有实际的文件,没有实际的路径,只能通过继承的方式 得到;
- 对应管道文件(p),可用于任意进程之间的通信;
- 打开管道时可指定读写方式;(指定读方式,返回的文件描述符只能用于读操作,指定读写方式,返回的文件描述符,即可用于读,也可用于写)
- 通过文件IO操纵,但是内容还是存放在内存中的;到管道(无名/有名)的读端与写端都关闭时,那在内存中的空间都会被自动释放;
- 管道文件大小永远是0;
- 当进程打开一个有名管道时,当管道只有读端或者写端时,进程会被阻塞,只有当读端与写端同时存在时,管道才是有意义的;
- 同一个系统下,有两个三个执行文件,可同时操作一个管道文件;
2、有名管道创建(熟练)
- 有名管道的创建 — mkfifo
#include <unistd.h>
#include <fcntl.h>
int mkfifo(const char *path);
— 成功时返回0,失败时返回EOF;
— path 创建的管道文件路径(相对路径,为空时默认 在当前位置 建立管道文件)
— mode 管道文件的权限,如0666(只有读写的权限 八进制)
3、有名管道读写(熟练)
- 管道如果存在的话,创建时会创建失败;
- 当管道读端 、写端只有一项单独存在时,进程在读、写该管道时会被阻塞,只有两端同时存在时,才能一起操作;
4、有名管道读写 — 示例
进程A :循环从键盘输入并写入有名管道myfifo,输入quit时退出
进程B :循环统计进程A每次写入myfifo的字符串的长度;
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
int main(int argc, const char *argv[])
{
if(mkfifo("myfifo",0666) < 0)
{
perror("mkfifo");
exit(-1);
}
return 0;
}
进程A:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#define N 32
int main(int argc, const char *argv[])
{
char buf[N];
int pfd;
if((pfd = open("myfifo",O_WRONLY))<0)
{
perror("open");
exit(-1);
}
while(1)
{
fgets(buf,N,stdin);
if(strcmp(buf,"quit\n") == 0)break;
write(pfd,buf,N);
}
close(pfd);
return 0;
}
进程B
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#define N 32
int main(int argc, const char *argv[])
{
char buf[N];
int pfd;
if((pfd = open("myfifo",O_RDONLY))<0)
{
perror("open");
exit(-1);
}
while(read(pfd,buf,N) > 0)
{
printf("the length of string is %d \n",strlen(buf));
}
close(pfd);
return 0;
}
三、信号机制
- 信号是软件层次对中断机制的一种模拟,是一种异步通信方式。
- 如果收到信号,程序会中止,处理收到的信号,处理完成后,继续执行后面的操作;
- Linux内核通过信号通知用户进程,不同的信号代表不同的事件;
- Linux对早期的Unix信号机制进行了扩展;前31种是早期Unix信号,没有使用排队机制,也称为不稳定机制;
- 进程对信号有不同的响应方式;
- 缺省方式:对于每种信号都有默认的缺省的处理方法;
- 忽略信号:收到信号后不处理;
- 捕捉信号(注册信号):系统收到信号后,会执行事先封装好的程序;
1、常用信号
2、信号发送命令
- 信号的相关命令 Kill/killall
- kill [-signal] pid
— 默认发送SIGTERM
— -sig可指定信号 ---- 可以给所有的进程发信号
— pid 指定发送对象;
例子:
kill -9 6437 向进程6437 发送类型为9的信号
kill -9 -8126 向8126进程组发送类型为9的信号
kill -9 -1 向除了系统中init与当前进程,所有的进程发送类型为9的信号;
- killall [-u user | prog]
- prog 指定进程名;
- user 指定用户名;
- 如果不是超级用户,只能结束/停止自己创建的进程;
SIGHUP/SIGINT/SIGKILL/SIGSTOP
killall :默认发送a.out 向系统所有运行a.out的进程发送信号;
killall -u linux : 向系统中linux用户创建的所有进程发送 15信号;
注: 当使用超级用户时,最好指定进程 发送信号,不然容易使得不该结束的进程提前结束,造 成更严重的问题;
3、信号发送
- 信号发送 — kill / raise
#include <unistd.h>
#include <signal.h>
int kill(pid_t pid,int sig);
int raise(int sig); — 向当前进程发送 信号量;
— 成功时返回0,失败时返回EOF;
— pid 接收进程的进程号; 0 代表同组进程;-1代表所有进程;
— sig信号类型;
4、创建定时器
- 信号相关函数 --- alarm/pause
int alarm(unsigned int seconds);
— 成功时返回上个定时器的剩余时间,失败时返回EOF;
— 在Linunx中,一个程序中只有一个定时器;使用该函数后,该进程上一次的定时器失效了,重新赋值计算;
— seconds 定时器的时间;
— 一个进程中只能设定一个定时器,时间到时产生SIGALRM
- int pause (void ); ------ 让进程睡眠
— 进程一直阻塞,直到被信号中断;
— 被信号中断后返回 -1 ,errno 为EINTR;
示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
alarm(3);
pause();
printf(“I hava waken up ! \n”);
reurn 0;
}
$ ./a.out
Alarm clock
重要:alarm 经常用于实现超时检测;
5、设置信号响应方式 - signal
- 将信号与 信号处理函数关联起来;
#include <unistd.h>
#include <signal.h>
void (*signal(int signo , void (*handler)(ing)))(int); --- 返回值是一个函数指针;
— 成功时返回原先的信号处理函数,失败时返回SIG_ERR;
— signo 要设置的信号类型
— handler指定的信号处理器函数:SIG_DFL代表缺省方式;
— SIG_IGN代表 忽略信号;
— 该函数将信号与信号处理函数相关联起来,进程接收到这个信号,然后去执行这个指定的信号处理函数;
6、信号捕捉
- 如何去捕捉一个信号的示例:
不同的信号可以指定相同的处理函数;
在函数处理函数中,判断收到该信号是哪个信号;
void handler(int signo)
{
if(signo == SIGINT)
{
printf(“I have got SIGINT !\n”);
}
if(signo == SIGQUIT)
{
printf(“I have got SIGQUIT!\n”);
}
}
int main()
{
signal(SIGINT,handler);
signal(SIGQUIT,handler);
while(1)pause;
return 0;
}
- signal 一旦设置了信号与信号处理函数的关联,将会持续有效;
7、小结
kill/raise : 任何进程发送信号/只能对当前进程发送信号;
alarm :设置定时器
signal : 设置信号的响应方式;