目录
一.什么是进程间通信
1.1什么是进程间通信
进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间 的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程 间通信( IPC:Inter Processes Communication )。
1.2进程间通信的目的
◼ 数据传输:一个进程需要将它的数据发送给另一个进程。
◼ 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种 事件(如进程终止时要通知父进程)。
◼ 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同 步机制。
◼ 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制 进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
1.3进程间通信方式
二.管道通信
2.1匿名管道
管道也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式, 所有的 UNIX 系统都支持这种通信机制。
例如需要统计一个目录中文件的数目命令:ls | wc –l,为了执行该命令,shell 创建了两 个进程来分别执行 ls 和 wc。
2.2管道的特点
◼ 管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的 操作系统大小不一定相同。
◼ 管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体, 但不存储数据。可以按照操作文件的方式对管道进行操作。
◼ 一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据 的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
◼ 通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺 序是完全一样的。
◼ 在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。 ◼ 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写 更多的数据,在管道中无法使用 lseek() 来随机的访问数据。
◼ 匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘 关系)之间使用。
◼ 管道默认是阻塞的,管道中没有数据时,read阻塞,管道数据存满之后,write阻塞。
2.3管道间通信的原理
2.4匿名管道的使用
2.5管道的读写特点
管道的读写特点
使用管道时需要注意以下特殊情况,默认是阻塞I/O操作。
1. 所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0)
如果有进程从管道读端读取数据,那么当管道中剩余的数据被全部读完之后,再次read会返回0(类似于读取文件时到达文件末尾)。
2. 如果有指向管道写端的文件描述符没有关闭(管道写端引用计数大于0),而持有管道写端文件描述符的进程也没有向管道中写数据,
那么此时有进程读取数据时,那么当管道中剩余的数据被全部读完之后,再次read时会阻塞,直到管道中有数据可以读取时才读取数据并返回。
3. 所有的指向管道读端的文件描述符都关闭了(管道读端引用计数为0),
此时有进程向管道中写数据时,该进程会收到一个信号SIGPIPE,通常会导致进程异常终止。
4. 如果有指向管道读端的文件描述符没有关闭(管道读端引用计数大于0),而持有读端文件描述符的进程也没有从管道中读数据,
如果有进程向管道中写数据,那么在管道被写满之后再次write会阻塞,直到管道中有空位置才能再次写入数据。总结:
读管道:
管道中有数据,read返回实际读取到的字节数
管道无数据:
写端全部关闭,read返回0,相当于读到文件末尾
写端没有完全关闭,read阻塞等待。
写管道:
管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有全部关闭:
管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数
设置管道非阻塞
//设置管道非阻塞
int flags=fcntlfd([0],F_GETFL); //获取原来的flag
flags|=O_NONBLOCK; //修改flags
int ret=fcntl(fd[0],F_SETFL,flags); //设置新的flags
2.6 有名管道
◼ 匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提 出了有名管道(FIFO),也叫命名管道、FIFO文件。
◼ 有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样 即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此 通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。
◼ 一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的 I/O系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一 个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的 名称也由此而来:先入先出。
◼ 有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,不一样的地方在于:
1. FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。
2. 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
3. FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。
2.6.1有名管道注意事项
有名管道的注意事项:
1. 一个为只读而打开的管道会阻塞,直到另外一个进程以只写打开管道
2. 一个为只写而打开的管道会阻塞,直到另一个进程以只读打开管道
读管道:
管道中有数据,read返回实际读取到的字节数
管道中无数据:
管道写端被全部关闭,read返回0(相当于读到文件末尾)
写端没有全部被关闭,read阻塞等待
写管道:
管道读端被全部关闭,进程异常终止(收到一个SIGPIPE信号)
管道没有全部关闭:
管道已经满了,write会阻塞
管道未满,write将数据写入,并返回实际写入的字节数
/*
创建fifo文件
1. 通过命令:mkfifo
2. 通过函数 mkfifo
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数:
- pathname: 管道路径
- mode: 文件权限 和open函数中的mode一样 八进制数
*/
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include<stdlib.h>
int main(){
//判断文件是否存在
int res=access("fifo1",F_OK); //判断是否存在
if(res==-1){
printf("管道不存在,创建管道\n");
int ret=mkfifo("fifo1",0664);
if(ret==-1){
perror("mkfifo");
return -1;
}
}else{
printf("管道已存在,不能创建!\n");
}
return 0;
}
三.内存映射
◼ 内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改 内存就能修改磁盘文件。
3.1 内存映射主要函数介绍
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
- 功能:将一个文件或设备的数据映射到内存中
- 参数:
- addr:NULL,由内核指定
- length:要映射的数据的长度,不能为0,建议使用文件的长度
获取文件长度,stat,lseek
- prot:对申请的内存映射区的操作权限
- PROT_EXEC:可执行权限
- PROT_READ:读权限
- PROT_WRITE:写权限
- PROT_NONE:没有权限
要操作映射内存必须要有读权限,
- flags:
- MAP_SHARED:映射区数据会自动和磁盘文件进行同步,进程间通信,必须要设置该选项
- MAP_PRIVATE:不同步,内存映射区的数据改变了,原来的文件不会修改,会重新创建一个新的文件(copy on write)
- fd:需要映射的那个文件的文件描述符
- 通过open得到,open的是一个磁盘文件
- 文件大小不能为0,open指定的权限不能和prot冲突
prot:PROT_READ open:读/读写
prot:PROT_READ|PROT_WRITE open:读写
- offset:映射文件的偏移量,一般不用(需要指定4k的整数倍,0表示不偏移)
- 返回值:
成功:返回创建的内存首地址
失败:MAP_FAILED (that is, (void *) -1) is returned,
int munmap(void *addr, size_t length);
- 功能:释放内存映射
- 参数:
- addr:要释放的内存的首地址
- length:要释放的内存的大小,要和mmap函数中length参数一致
3.2 内存映射注意事项
◼ 如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
void * ptr=mmap(...);
可以对ptr进行++操作,ptr++;
但在销毁指针时会出错,munmap(ptr,len); 需要提前保存++后的地址,释放该地址才可以。
◼ 如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
错误,返回MAP_FAILED
open()函数中的权限建议和port参数权限保持一致
◼ 如果文件偏移量为1000会怎样?
偏移量必须是1024的整数倍,返回MAP_FAILED
◼ mmap什么情况下会调用失败?
- 第一个参数:length=0
- 第三个参数:port
- 只设置了写权限
- port=PORT_READ|PORT_WRITE
第5个参数fd 通过open函数指定的 O_RDONLY / O_WRONLY
◼ 可以open的时候O_CREAT一个新文件来创建映射区吗?
- 可以的,但是创建的文件的大小如果为0的话,那就不符合要求
- 可以对新的文件进行扩展
- lseek()
- truncate()
◼ mmap后关闭文件描述符,对mmap映射有没有影响?
int fd=open("XXX");
mmap(,,,,fd,0);
close(fd);
映射区还存在,创建映射区的fd被关闭,没有任何影响
◼ 对ptr越界操作会怎样?
void * prf=mmap(NULL,100,,,,);
越界操作使用的是非法内存,会引发段错误.
四. 信号
4.1 信号的基本概念
◼ 信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也 称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号 可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
◼ 发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C 通常会给进程发送一个中断信号。
硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给 相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的 内存区域。
系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
运行 kill 命令或调用 kill 函数。
4.2 信号的主要作用
◼ 使用信号的两个主要目的是:
让进程知道已经发生了一个特定的事情。
强迫进程执行它自己代码中的信号处理程序。
◼ 信号的特点:
简单
不能携带大量信息
满足某个特定条件才发送
优先级比较高
◼ 查看系统定义的信号列表:kill –l
◼ 前 31 个信号为常规信号,其余为实时信号。
4.3 信号的默认处理动作
◼ 查看信号的详细信息:man 7 signal
◼ 信号的 5 中默认处理动作
Term 终止进程
Ign 当前进程忽略掉这个信号
Core 终止进程,并生成一个Core文件
Stop 暂停当前进程
Cont 继续执行当前被暂停的进程
◼ 信号的几种状态:产生、未决、递达
◼ SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。
4.4 与信号相关的主要函数介绍
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
- 功能:给某个进程或者进程组pid,发送某个信号sig
- 参数:
- pid:需要发送信号的目标进程
> 0: 将信号发送给指定的进程
= 0:将信号发送给当前进程组
= -1:将信号发送给每一个有权限接收该信号的进程
< -1:对其取反获得进程组id,将信号发送给对应的进程组
- sig:需要发送的信号的编号或者是宏值,同一个信号在不同架构下编号不一样,所以最好使用宏值
0 代表不发送信号
kill(getppid(),9);
kill(getpid(),9);
int raise(int sig);
- 功能:给当前进程发送信号
- 参数:
- sig:要发送的信号
- 返回值:
- 成功:0
- 失败:0
kill(getpid(),sig)
void abort(void);
- 功能:发送SIGABRT信号给当前进程,杀死当前进程
kill(getpid(),SIGABRT);
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
- 功能:设置定时器(闹钟) 函数调用后开始倒计时,倒计时为0后函数会给当前进程发送信号:SIGALARM
- 参数:该函数不会阻塞
seconds:倒计时的时长,单位:秒,如果参数为0,定时器无效(不进行倒计时,不发送信号)
取消定时器,通过alarm(0);
- 返回值:
- 之前有定时器,返回之前定时器倒计时剩余的时间
- 之前没有定时器,返回0
SIGALARM:默认终止当前进程,每一个进程都有且只有唯一的一个定时器
alarm(10);
过了一秒
alarm(5); //重置定时器,返回上一个定时器剩余的时间
alarm(100); -> 该函数是不阻塞
#include <sys/time.h>
int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
- 功能:设置定时器(闹钟),可以替代alarm函数
- 参数:r
- which:以什么时间计时
ITIMER_REAL:真实时间,时间到达,发送 SIGALRM 常用
ITIMER_VIRTUAL:用户时间,时间到达,发送 SIGVTALRM
ITIMER:PROF:以该进程在用户态和内核态下所消耗的时间来计算,时间到达。发送 SIGPROF
- new_value:设置定时器的属性
struct itimerval { //定时器结构体
struct timeval it_interval; //间隔时间,每个阶段的时间(本次定时器执行结束后多久执行下一次定时器)
struct timeval it_value; //延长多长时间执行定时器(多长时间后开始执行定时器)
};
struct timeval { //时间结构体
time_t tv_sec; //秒数
suseconds_t tv_usec;//微秒
};
- old_value:记录上一次定时的时间参数,一般不使用,指定NULL
//过10秒后,每个两秒定时一次
- 返回值:
成功 0
失败 -1,并设置错误号
/*
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- 功能:设置某个信号的捕捉行为
- 参数:
- signum:要捕捉的信号
- handler:捕捉到信号要如何处理
- SIG_IGN:忽略该信号
- SIG_DFL:执行信号默认的行为
- 回调函数:该函数是内核调用,程序员只负责实现该函数
- 返回值:
成功,返回上一次注册的信号处理函数的地址,第一次调用返回NULL
失败,返回SIG_ERR,设置错误号
SIGKILL,SIGSTOP 不能被捕捉,不能被忽略
*/
#include <sys/time.h>
#include <stdio.h>
#include<stdlib.h>
#include<signal.h>
//typedef void (*sighandler_t)(int);
void myalarm(int num){
printf("捕捉到的信号编号是:%d\n",num);
printf("29...\n");
}
//过3秒之后,每隔2秒定时一次
int main(){
//注册信号捕捉
//typedef void (*sighandler_t)(int)=myalarm;
__sighandler_t sit=signal(SIGALRM,myalarm);
if(sit==SIG_ERR){
perror("signal!!");
exit(0);
}
struct itimerval new_value;
//设置值
//设置定时的间隔时间
new_value.it_interval.tv_sec=2;
new_value.it_interval.tv_usec=0;
//设置延迟的时间,
new_value.it_value.tv_sec=3;
new_value.it_value.tv_usec=0;
int ret=setitimer(ITIMER_REAL,&new_value,NULL);
printf("定时器开始了!\n");
if(ret==-1){
perror("setitimer");
exit(0);
}
getchar();
return 0;
}
4.5 内核实现信号捕捉的过程
五.共享内存
◼ 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于 一个共享内存段会称为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其 他所有共享同一个段的进程可用。
◼ 与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据 从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。
5.1 共享内存使用步骤
◼ 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其 他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
◼ 使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
◼ 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存, 程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间 中该共享内存段的起点的指针。
◼ 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存 了。这一步是可选的,并且在进程终止时会自动完成这一步。
◼ 调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之 后内存段才会销毁。只有一个进程需要执行这一步。
5.2 共享内存常用操作函数
共享内存操作函数 #include <sys/ipc.h> #include <sys/shm.h> ◼ int shmget(key_t key, size_t size, int shmflg); - 功能:创建一个新的共享内存段,或者获取一个已有的共享内存段的标识, 新创建的内存段中的数据会被初始化为0. - 参数: - key:key_t类型是一个整型,可通过该参数创建或找到一个共享内存 一般使用16进制表示,非0值。 - size:共享内存的大小, - shmflg: - 访问权限 - 附加属性:创建/判断共享内存是否存在 - 创建:IPC_CREAT - 判断共享内存是否存在:IPC_EXCL,需要和IPC_CREAT一起使用 IPC_CREAT | IPC_EXCL | 0664 - 返回值: 失败:-1 设置错误号 成功:>0 返回共享内存的引用的ID,后面操作共享内存都会使用该值。 ◼ void *shmat(int shmid, const void *shmaddr, int shmflg); - 功能:和当前进程进行关联 - 参数: - shmid:共享内存的标识(ID),由shmget返回值获取 - shmaddr:申请的共享内存在虚拟内存中的首地址,指定NULL,由内核指定。 - shmflg:对共享内存的操作 - 读:SHM_RDONLY,必须要有读权限 - 读写:0 - 返回值: 成功:返回共享内存的首地址 失败:(void *) -1 ◼ int shmdt(const void *shmaddr); - 功能:解除当前进程和共享内存的关联 - 参数: shmaddr:共享内存的首地址 - 返回值: 成功:0 失败:-1 ◼ int shmctl(int shmid, int cmd, struct shmid_ds *buf); - 功能:对共享内存进行操作,可删除共享内存,共享内存要删除才会消失,创建共享内存的进程被销毁对共享内存没有影响 - 参数: - shmid:共享内存的ID - cmd:要执行的操作 - IPC_STAT:获取共享内存当前状态 - IPC_SET:设置共享内存状态 - IPC_RMID:标记该共享内存需要销毁(当所有进程都取消与该共享内存的关联后才会真正销毁) - buf:需要设置或获取的共享内存的属性信息 cmd=IPC_STAT时buf存储数据, cmd=IPC_SET时,buf中需要初始化数据,并设置到内存中 cmd=IPC_RMID时,不设置buf,置为NULL struct shmid_ds { struct ipc_perm shm_perm; // Ownership and permissions size_t shm_segsz; // Size of segment (bytes) time_t shm_atime; // Last attach time time_t shm_dtime; // Last detach time time_t shm_ctime; // Last change time pid_t shm_cpid; // PID of creator pid_t shm_lpid; // PID of last shmat(2)/shmdt(2) shmatt_t shm_nattch; // No. of current attaches 记录当前与共享内存关联的进程数量 ... }; ◼ key_t ftok(const char *pathname, int proj_id); - 功能:根据指定的路径名和int值,生成一个共享内存的key。 - 参数: - pathname:指定一个存在的路径 /home/nowcoder/Linux/a.txt - proj_id:int类型的值,但系统调用只会使用其中的一个字节(8个位) 范围:0-255 一般指定一个字符 'a' 1.操作系统如何知道一块共享内存被多少进程关联 - 共享内存维护了一个结构体struct shmid_ds 该结构体中有一个成员 shm_nattch - shm_nattch 记录了当前与共享内存相关联的进程数目 2.可以对共享内存进行多次删除吗 - 可以的 - 因为 shmctl 只是标记要删除对应的共享内存,不是直接删除。 - 只有当与该共享内存关联的进程数为0时,才会真正被删除。 共享内存和内存映射的区别: 1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外) 2.共享内存效率更高 3.内存 所有的进程操作的是同一块共享内存 内存映射,每个进程在自己的虚拟地址空间中都有一个独立的内存。 4.数据安全 - 进程突然退出 共享内存还存在 内存映射区不存在 - 运行进程的电脑宕机 数据存储在共享内存中,没有了 由于磁盘文件中的数据还在,所以内存映射区中的数据还存在 5.生命周期 - 内存映射区:进程退出,内存映射区销毁 - 共享内存:进程退出,共享内存还在,需要手动删除(所有的关联的进程数为0),或者关机 如果一个进程退出,会自动和共享内存取消关联。