一.进程间通信(IPC)分类
1.古老的通信方式
-
无名管道(Pipes)
- 只能用于有亲缘关系的进程间通信。
- 数据流是单向的,通常用于父子进程间。
-
有名管道(Named Pipes,FIFO)
- 可以用于任意两个进程间通信,即使它们没有亲缘关系。
- 允许不相关的进程以先进先出(FIFO)的方式发送和接收数据。
-
信号(Signals)
- 一种异步通信机制,用于通知进程有某些事件发生。
- 信号可以在任何时候发送给进程,接收进程可以捕获并处理信号。
2.IPC对象通信
-
消息队列(Message Queues)
- 用于进程间的消息传递。
- 消息队列允许进程发送和接收格式化的消息。
-
共享内存(Shared Memory)
- 允许多个进程共享一个给定的存储区。
- 这是最快的IPC形式,因为避免了数据的复制。
-
信号量集(Semaphores)
- 用于控制对共享资源的访问。
- 信号量可以用来实现同步和互斥。
3.Socket通信
- Sockets
- 用于网络通信,也可用于本机进程间通信。
- 提供了一种双向通信的机制,支持面向连接(如TCP)和无连接(如UDP)的通信。
4.特殊说明
- 信号是古老通信方式中唯一的异步通信方式。
- 共享内存是所有通信方式中最有效的方式。
二.管道通信
- 无名管道:只能用于有亲缘关系的进程间通信。
- 有名管道(FIFO):可以用于任意单机进程间通信。
1.管道特性
- 管道是半双工工作模式。
- 所有的管道都是特殊文件,不支持定位操作。(×)lseek->> fd fseek ->>FILE*
- 管道读写使用文件IO。//fgets,fread,fgetc,open,read,write,close;
2.阻塞破裂
-
写入阻塞
当管道的读端存在时,如果向管道写入的数据量超过了管道的容量限制(通常是64KB),写操作将被阻塞,直到管道中的数据被读取,为新写入的数据腾出空间。 -
读取阻塞
如果管道的写端存在,而读端尝试读取管道中的数据,但管道为空,读操作将被阻塞,直到有数据可读。 -
管道破裂
当管道的读端关闭后,写端如果尝试写入数据,将触发SIGPIPE
信号,导致写入进程异常终止或退出。这被称为“管道破裂”,因为管道的一端不再存在,无法完成数据传输。 -
读取返回0
当管道的写端关闭,且管道内的数据已被完全读取,此时如果继续读取管道,read
函数将返回0,表示没有更多的数据可读。这并不意味着管道已经“破裂”,而是表明管道已被清空。
3.管道使用框架
- 创建管道
- 读写管道
- 关闭管道
三.无名管道
- 使用
pipe
函数创建。 - 亲缘关系进程使用,有固定的读写端。
1.创建并打开管道
-
使用
pipe()
函数创建一个无名管道。 -
包含头文件
<unistd.h>
。
#include <unistd.h>
int pipe(int pipefd[2]);
pipefd[0]
:无名管道的固定读端。pipefd[1]
:无名管道的固定写端。- 返回值:成功返回 0,失败返回 -1。
2.读写管道
- 使用文件 IO 的读写方式。
- 读端使用
read()
函数读取数据。 - 写端使用
write()
函数写入数据。
3.关闭管道
- 使用
close()
函数关闭管道的读端或写端。 fd
:要关闭的文件描述符(可以是读端或写端)。
- 注意:
- 无名管道的创建应在
fork()
调用之前进行,以确保父子进程能够正确地使用管道两端。
- 无名管道的创建应在
四.有名管道
- 使用
mkfifo
创建。 - 可以在文件系统中可见。
1.有名管道操作框架
1.创建有名管道
- 使用
mkfifo
函数创建有名管道。 - 函数原型:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
- 参数:
pathname
: 有名管道的路径和名称。mode
: 有名管道的八进制文件权限。
- 返回值:
- 成功: 返回 0。
- 失败: 返回 -1,并设置
errno
。
2.打开有名管道
- 使用
open
函数打开有名管道。 - 注意只能以只读(
O_RDONLY
)或只写(O_WRONLY
)模式打开,不能使用O_RDWR
。
3.读写管道
- 使用文件 IO 函数进行读写操作。
4.关闭管道
- close(fd);
5.卸载管道
- remove();
int unlink(const char *pathname);
功能:将指定的pathname管道文件卸载,同时从文件系统中删除。
参数: ptahtname 要卸载的有名管道 。
返回值:成功为0,失败为-1。
2.问题思考
1. 同步需求和位置
- 同步的必要性:有名管道的读写操作通常需要同步,以确保数据能够被正确发送和接收。
- 读端关闭:如果读端关闭,写端将不能继续写入数据。在大多数系统中,写入有名管道而没有打开读端会导致写入操作失败,可能触发
SIGPIPE
信号,导致写入进程终止。 - 写端关闭:如果写端关闭,读端可以继续读取管道中已有的数据,直到数据被完全读取。如果管道中没有数据,读端的读取操作将返回0,表示没有更多数据可读。
2.有名管道在fork之后的亲缘关系进程中的使用
- 亲缘关系进程间使用:有名管道可以在fork之后创建的亲缘关系进程间使用,例如父子进程或兄弟进程。
- 启动次序:进程启动的次序可能会影响同步。例如,如果写进程先于读进程启动并尝试写入数据,可能会因为读进程尚未打开管道而阻塞写操作。
3. 手工操作有名管道实现数据传送
- 读取有名管道:可以使用
cat
命令读取有名管道中的数据。 - 写入有名管道:可以使用
echo
命令写入有名管道。
cat fifoname
echo "data" > fifoname
五.信号通信
信号通信概述
- 应用:用于异步通信和中断处理。
- 信号范围:1~64,其中前32个信号有具体含义。
信号的默认响应
- Term:默认操作是终止进程。
- Ign:默认操作是忽略信号。
- Core:默认操作是终止进程并转储核心(dump core),可用于调试。
- Stop:默认操作是停止进程。
- Cont:如果进程当前已停止,则继续执行。
1. 发送端
kill
函数
- 函数:int kill(pid_t pid, int sig);
- 功能:通过该函数可以给pid进程发送信号为sig的系统信号。
- 参数:
pid
:目标进程的PID(进程标识符)。如果PID为负数,则信号将发送给与PID绝对值相等的进程组中的所有进程。sig
:当前程序要发送的信号编号。
- 返回值:
- 成功:返回0。
- 失败:返回-1,并设置
errno
以指示错误。
raise
函数
- 函数:int raise(int sig) == kill(getpid(),int sig);
- 功能:等同于
kill(getpid(), sig)
,即发送信号给调用进程本身。 - 参数:
sig
,要发送给当前进程的信号编号。
alarm
函数
- 函数:unsigned int alarm(unsigned int seconds);SIGALAM
- 功能:设置一个定时器,经过指定的秒数后,向调用进程发送
SIGALRM
信号。闹钟只有一个, 定时只有一次有效,但是必须根据代码逻辑是否执行判断。 - 参数:
seconds
,定时器的秒数。 - 返回值:返回之前设置的定时器剩余秒数,如果之前没有设置,则返回0。
pause
函数
- 函数:int pause(void);
- 功能:使调用进程挂起,直到收到信号。
2. 信号
- 信号是UNIX和类UNIX系统中用于进程间通信的一种机制。
- 信号可以由操作系统生成(如
SIGINT
、SIGTERM
等),也可以由其他进程通过kill
函数发送。 - 信号列表可以通过
kill -l
命令查看,前32个信号具有特定的默认行为和含义。
3. 接收端
信号处理方式
- 默认处理:每个信号都有一个默认行为,例如
SIGINT
默认会终止进程。 - 忽略处理:进程可以选择忽略某些信号,例如
SIGCONT
和SIGSTOP
。 - 自定义处理:进程可以注册自定义的信号处理函数来响应信号。
信号注册函数
- 原型:
void ( *signal(int signum, void (*handler)(int)) ) (int);
- 参数:
signum
:要注册信号处理的信号编号。handler
:指向信号处理函数的指针。
- 返回值:返回指向之前信号处理函数的指针,如果之前没有注册,则返回
SIG_ERR
。
信号处理函数的宏
SIG_DFL
:指定信号的默认处理。SIG_IGN
:指定信号被忽略。handler
:指定自定义的信号处理函数。
自定义信号处理
1、必须事先定义自定义函数,必须是如下格式:
void fun(int sig) sig 接收到的信息编号
{
}
2、在所有的信号中有如下两个特列:
10 SIGUSR1
12 SIGUSR2
专门预留给程序员使用的未定义信号。