进程间通信
管道
匿名管道「|」
ps auxf | grep mysql
上面这种管道是没有名字,所以「|」表示的管道称为匿名管道,用完了就销毁
命名管道(mkfifo)
mkfifo myPipe
myPipe 就是这个管道的名称,基于 Linux 一切皆文件的理念,所以管道也是以文件的方式存在,我们可以用 ls 看一下,这个文件的类型是 p,也就是 pipe(管道) 的意思
1 | ls -l |
接下来,我们往 myPipe 这个管道写入数据
1 | echo "hello" > myPipe // 将数据写进管道 |
因为管道里的内容没有被读取,只有当管道里的数据被读完后,命令才可以正常退出
1 | cat < myPipe // 读取管道里的数据 |
优缺点:
- 优点:自然就是简单,同时也我们很容易得知管道里的数据已经被另一个进程读取了
- 缺点:通信方式效率低,不适合进程间频繁地交换数据
原理
匿名管道的创建,需要通过下面这个系统调用
1 | int pipe(int fd[2]) |
这里表示创建一个匿名管道,并返回了两个描述符,一个是管道的读取端描述符 fd[0],另一个是管道的写入端描述符 fd[1]。注意,这个匿名管道是特殊的文件,只存在于内存,不存于文件系统中
所谓的管道,就是内核里面的一串缓存。我们可以使用 fork 创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个 fd[0] 与 fd[1],两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了
管道只能一端写入,另一端读出,所以上面这种模式容易造成混乱,因为父进程和子进程都可以同时写入,也都可以读出。那么,为了避免这种情况,通常的做法是: 父进程关闭读取的 fd[0],只保留写入的 fd[1]; 子进程关闭写入的 fd[1],只保留读取的 fd[0]
到这里,我们仅仅解析了使用管道进行父进程与子进程之间的通信,但是在我们 shell 里面并不是这样的。 在 shell 里面执行 A | B 命令的时候,A 进程和 B 进程都是 shell 创建出来的子进程,A 和 B 之间不存在父子关系,它俩的父进程都是 shell
消息队列
消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核中的消息链表(内核是共享的),消息队列的消息体可以是用户自定义的数据类型。在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),当接收方接受数据时,也要与发送方的数据类型一样
缺点:
- 通信不及时,因为每次数据的写入和读取都需要经过用户态和内核态的拷贝过程
- 不适合大数据的传输,因为内核中的消息体都有一个最大长度的限制,同时所有队列包含的消息体也是有上限的
共享内存
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,大大提高了进程间通信的速度
优缺点:
- 优点:不需要陷入内核态或者系统调用,也不需要拷贝数据
- 缺点:多进程竞争同一个资源会造成数据的错乱
信号量
对于共享内存的多进程竞争资源,而造成数据错乱的问题。信号量可以解决
信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据
信号量表示资源的数据,有两种操作:
- P操作:这个操作会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行
- V操作:这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程
- 总的来说,操作后:如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行
P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的
共享和互斥的实现方式:
- 如果要使得两个进程互斥访问共享内存,我们可以初始化信号量为1
- 如果要实现多进程同步的方式,我们可以初始化信号量为0
信号
信号跟信号量虽然名字相似度 66.66%,但两者用途完全不一样,就好像 Java 和 JavaScript 的区别
在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号:
运行在 shell 终端的进程,我们可以通过键盘输入某些组合键的时候,给进程发送信号。例如
- Ctrl+C 产生SIGINT信号,表示终止该进程
- Ctrl+Z 产生SIGTSTP信号,表示停止该进程,但还未结束
如果进程在后台运行,可以通过 kill 命令的方式给进程发送信号,但前提需要知道运行中的进程 PID 号,例如: kill -9 1050 ,表示给 PID 为 1050 的进程发送 SIGKILL 信号,用来立即结束该进程
信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式
- 执行默认操作: Linux对每种信号都做了默认操作,比如列表中的SIGTERM信号,就是终止进程的意思
- 捕捉信号:我们可以定义信号是一个信号处理函数,当信号发生的时候就执行对应的函数
- 忽略信号:当我们不希望处理信号的时候就可以忽略,但有两个是不能捕捉和忽略的,分别是SIGKILL和SEGSTOP,分别表示任何时候中断或者结束某一进程
Socket
socket函数
int socket(int domain, int type, int protocal)
socket参数意义:
- domain:用来指定协议族,比如 AF_INET 用于 IPV4、AF_INET6 用于 IPV6、AF_LOCAL/AF_UNIX 用于本机
- type:参数用来指定通信特性,比如 SOCK_STREAM 表示的是字节流,对应 TCP、SOCK_DGRAM 表示的是数据报,对应 UDP、SOCK_RAW 表示的是原始套接字
- protocal:参数原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成,protocol 目前一般写成 0 即可
根据创建 socket 类型的不同,通信的方式也就不同:
- 实现 TCP 字节流通信: socket 类型是 AF_INET 和 SOCK_STREAM
- 实现 UDP 数据报通信:socket 类型是 AF_INET 和 SOCK_DGRAM
- 实现本地进程间通信: 「本地字节流 socket 」类型是 AF_LOCAL 和 SOCK_STREAM,「本地数据报 socket 」类型是 AF_LOCAL 和 SOCK_DGRAM。另外,AF_UNIX 和 AF_LOCAL 是等价的,所以 AF_UNIX 也属于本地 socket
针对 TCP 协议通信的 socket 编程模型
- bind:服务端用于将把用于通信的地址和端口绑定到 socket 上
- 这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket
针对 UDP 协议通信的 socket 编程模型
UDP 是没有连接的,所以不需要三次握手,也就不需要像 TCP 调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口号,因此也需要 bind
针对本地进程间通信的 socket 编程模型
本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是绑定一个本地文件,这也就是它们之间的最大区别