进程
进程间的通信方式
- 管道
- 消息
- 共享内存
- 信号量
- 信号
- socket
优缺点以及应用场景
管道
匿名管道
1、命令中使用的“|”就是一种匿名管道,用完就销毁,如:
ps -ef | grep jboss
2、匿名管道的创建
#include<unistd.h>
int pipe(int pipefd[2]);
其中,pipefd[2]为这个数组的传出参数:pipefd[0]对应管道的读端,pipefd[1]对应管道的写端。
该函数的返回值为:0,-1。-1表示匿名管道创建失败。
查看管道缓存大小命令:
ulimit -a
查看管道缓存大小的方法:
#include<unistd.h>
long fpathconf(int fd, int name);
特点:
- 匿名管道只能在具有公共祖先的进程之间使用,如父子进程,兄弟进程。
- 匿名管道没有文件实体,只存在于内存。
- 操作管道的进程被销毁后,内存自动释放
- 管道默认是阻塞的,读阻塞,写阻塞。
- 管道的实现是通过环队列,一头进,一头出。
- 数据只能读取一次,不能重复读取。
- 数据是双办公,一方写入,一方读取。
命名管道
也被叫做FIFO,因为数据采用先进先出的传输方式。对于命名管道。他可以在不想关的进程中通信。
使用命名管道前需要先使用mkfifo命令来创建,并指定管道名字。
1、使用shell命令创建:
mkfifo myPipe
2、使用方法创建:
#include<unistd.h>
int mknod(const char *path, mode_t mod, dev_t dev);
int mkinfo(const char *path, mode_t mode);
path为管道文件路径。
mode为权限,一般使用S_IFIFO|0666
dev为该文件对应的设备文件的设备号,普通文件都为0。
myPipe就是这个管道的名称,以文件的方式存在,使用ls查看,该文件以p(pipe)开头。
当一个进程在myPipe里写入内容之后,只有当另一个进程读取完数据后,该进程才可以继续运行。
所以管道这种通信方式效率低,不适合进程之间频繁的交换数据。优点就是简单,可以及时得知管道里的数据有没有被读取。
因为管道默认阻塞,如果需要设置成非阻塞,则使用fcntl变参函数
1、获取原来的函数参数
int flags = fcntl(f[0], F_GETFL);
2、设置新的flags
flags |= O_NONBLOCK;
3、修改参数
fcntl(f[0], F_SETGL, flags);
消息队列
适用场景:需要频繁的交换数据(通信模式)。
消息队列是保存在内核中的消息链表,在发送数据时会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,每个消息体都是固定大小的存储快,如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息对列会一直存在。
消息队列的缺点:
- 通信不及时。
- 不适合比较大的数据传输。
- 消息队列通信过程中,存在用户态与内核态之间的数据贝开销。
消息队列使用函数
#include<sys/typs.h>
#include<sys/ipc.h>
#include<sys/msg.h>
1、创建活获取一个消息对列,成功返回消息队列ID,失败返回-1; msgflg:IPC_CREAT
int msgget(key_t key, int msgflg);
2、发送一条消息,成功返回0,失败返回-1。msgqp为消息体,msgsz为消息体长度,msgflg一般设置为0,也可以设置IPC_NOWAIT
int msgsnd(int msgid, const void *msqp, size_t msgsz, int msgflg);
3、接收一条消息,成功返回消息数据长度,失败返回-1。 msgqp为消息体,msgsz为消息体长度,msgflg一般设置为0,也可以设置IPC_NOWAIT,msgtyp:指定接收消息类型,类型可以为0
ssize_t msgrcv(int msgid, void *msgqp, size_t msgsz, long msgtyp, int msgflg);
4、控制消息队列,成功返回0,失败返回-1。cmd:IPC_RMID
int msgctl(int msgid, int cmd, struct msgid_ds *buf);
共享内存
消息队列的读取和写入,都会发生用户态和内核态之间的消息拷贝过程。那共享内存的方式就解决了这一切。
现在操作系统,采用虚拟内存技术进行内存管理,也就是每个进程有自己的虚拟内存空间,不同进程的虚拟内存映射到部提的物理地址中。所以即使不同进程之间的虚拟地址一样,其实访问的是不同的物理地址,对于数据的增删改查不影响。
共享内存的操作就是拿出一块空白的虚拟地址空间,映射到相同的物理内存中。这个一个进程写入的东西,另一个进程立刻就会看到,不需要数据拷贝。增加了进程间通信的速度。
信号量
共享内存可能会有一个新的问题就是,如果多个进程同时修改一个共享内存,那就有可能会有冲突,如果两个进程同时写一个地址,那先写的那个会被后写的那个覆盖。
此时就需要一个保护机制,方式多个进程竞争共享资源。信号量就实现了这一机制。
信号量就一个整形的计数器,主要用于实现进程之间的同步与互斥,而不是用于缓存进程之间通信的数据
信号量表示资源的数量,控制信号量有两种原子操作:
- P操作,将信号量减1,如果信号量为负数,则表示资源已经占用,则需要阻塞等待。
- V操作,将信号量加1,相加后信号量仍旧为负数,则表示有进程阻塞,于是会唤醒进程运行。
P操作是进入共享资源之前,V操作是离开共享资源之后。
当信号量为1时,表示互斥信号量。可以保证共享内存任何时刻只有一个进程在访问。
当信号量为0是,表示同步信号量。
信号
对于上面说的通信,都是在常规状态下的工作模式。对于异常情况下的工作模式,就需要用信号的方式来通知进程。 例如:
- Ctrl+C产生SIGINT信号,表示终止进程。
- Ctrl+Z产生SIGTSTP信号,表示应该停止进程,但还未结束。
如果进程还在后台运行,可以通过kill命令给进程发送信号,表示结束进程。
信号是进程间通信机制中唯一的异步通信机制 因为可以在任何时刻发送信号给进程,有信号产生,我们就可以有以下几种对信号的处理方式
- 执行默认操作
- 捕捉信号
- 忽略信号
信号
管道、消息队列、共享内存,信号量,信号都是指在同一主机上运行的进程之间的通信,如果需要跨网络,则需要socket通信。
socket通信有三种方式:TCP字节流,UDP数据报流,本地进程间通信。
TCP通信建立连接是采用三次握手,而断开连接则采用4次挥手。
- 服务端和客户端初始化socket,得到文件描述符;
- 服务端调用bind,绑定IP地址和端口;
- 服务端调用listen,进行监听;
- 服务端调用accept,等待客户端连接;(当连接成功,会返回一个新的搜才可以连接用于后续数据传输);
- 客户端调用connect,向服务端发起连接请求;
- 服务端accept返回用于传输的socket的文件描述符;
- 客户端调用write写入数据,服务端调用read读数据;
- 客户端断开连接时,调用close,那么服务端读取的时候,会得到EOF,待处理完数据后,服务端调用close,表示关闭连接。
由socket通信过程可知,监听和传输数据是用的两个socket,一个叫监听socket,一个叫已完成连接socket。
UDP通信没有连接,不需要三次握手。
UDP不需要连接,也就没有所谓的发送方和接收方,不存在客户端和服务端,所以每个socket都要bind,传入IP和端口。
线程
由于同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做的线程之间的通信,比如全局变量,所有对线程关注的不是通信方式,而是多线程竞争共享资源的问题。
而竞争共享资源的保护措施就是锁,C++中主要有以下锁
- 互斥锁
- 条件锁
- 自旋锁
- 读写锁
- 递归锁
互斥锁
互斥锁用于控制多个线程对他们之间的共享资源互斥访问的一个信号量。
在某一时刻只有一个线程可以获得互斥锁,在释放互斥锁之前其他线程都不能获得互斥锁。
条件锁
条件锁就是所谓的条件变量。当一个线程某个条件未满足时,可以使用条件变量,是这个线程进入阻塞状态。一旦条件满足,则以信号量的方式唤醒一个因为该条件而阻塞的线程。
最为常见就是,在线程池中,初始情况下因为没有任务而使队列为空,此时线程池中线程以“任务队列为空”这个条件而进入阻塞状态,一旦有任务进来,则会以信号量的方式唤醒线程来处理这个任务。
自旋锁
由名字可知,如果一个线程获得自旋锁,另外的线程想要获得自旋锁,则需要占用CPU不停的访问这个自旋锁,直到获取这个锁为止,而不是进入阻塞状态。
由此可知,自旋锁相比于其他锁,比较耗费CPU。
读写锁
说到读写锁我们可以借助于“读者-写者”问题进行理解。首先我们简单说下“读者-写者”问题。
计算机中某些数据被多个进程共享,对数据库的操作有两种:一种是读操作,就是从数据库中读取数据不会修改数据库中内容;另一种就是写操作,写操作会修改数据库中存放的数据。因此可以得到我们允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。这就是一个简单的读者-写者模型。
递归锁
一般而言,所得功能与性能成反比。而且我们一般不使用递归锁,这里不做介绍。
进程线程的状态转换
(1)就绪状态:进程已获得除CPU外的所有必要资源,只等待CPU时的状态。一个系统会将多个处于就绪状态的进程排成一个就绪队列。
(2)执行状态:进程已获CPU,正在执行。单处理机系统中,处于执行状态的进程只一个;多处理机系统中,有多个处于执行状态的进程。
(3)阻塞状态:正在执行的进程由于某种原因而暂时无法继续执行,便放弃处理机而处于暂停状态,即进程执行受阻。(这种状态又称等待状态或封锁状态)
通常导致进程阻塞的典型事件有:请求I/O,申请缓冲空间等。
一般,将处于阻塞状态的进程排成一个队列,有的系统还根据阻塞原因不同把这些阻塞集成排成多个队列。
进程的三种基本状态的转换
(1) 就绪→执行
处于就绪状态的进程,当进程调度程序为之分配了处理机后,该进程便由就绪状态转变成执行状态。
(2) 执行→就绪
处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完而不得不让出处理机,于是进程从执行状态转变成就绪状态。
(3) 执行→阻塞
正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态变成阻塞状态。
(4) 阻塞→就绪
处于阻塞状态的进程,若其等待的事件已经发生,于是进程由阻塞状态转变为就绪状态。