管道

匿名管道「|」

ps auxf | grep mysql

上面这种管道是没有名字,所以「|」表示的管道称为匿名管道,用完了就销毁

命名管道(mkfifo)

mkfifo myPipe

myPipe 就是这个管道的名称,基于 Linux 一切皆文件的理念,所以管道也是以文件的方式存在,我们可以用 ls 看一下,这个文件的类型是 p,也就是 pipe(管道) 的意思

1
2
ls -l
prw-r--r--. 1 root root 0 Jul 17 02:45 myPipe

接下来,我们往 myPipe 这个管道写入数据

1
2
echo "hello" > myPipe  // 将数据写进管道
// 阻塞...

因为管道里的内容没有被读取,只有当管道里的数据被读完后,命令才可以正常退出

1
2
cat < myPipe  // 读取管道里的数据
hello

优缺点:

  • 优点:自然就是简单,同时也我们很容易得知管道里的数据已经被另一个进程读取了
  • 缺点:通信方式效率低,不适合进程间频繁地交换数据

原理

匿名管道的创建,需要通过下面这个系统调用

1
int pipe(int fd[2])

这里表示创建一个匿名管道,并返回了两个描述符,一个是管道的读取端描述符 fd[0],另一个是管道的写入端描述符 fd[1]。注意,这个匿名管道是特殊的文件,只存在于内存,不存于文件系统中

alt text

所谓的管道,就是内核里面的一串缓存。我们可以使用 fork 创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个 fd[0] 与 fd[1],两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了

alt text

管道只能一端写入,另一端读出,所以上面这种模式容易造成混乱,因为父进程和子进程都可以同时写入,也都可以读出。那么,为了避免这种情况,通常的做法是: 父进程关闭读取的 fd[0],只保留写入的 fd[1]; 子进程关闭写入的 fd[1],只保留读取的 fd[0]

alt text

到这里,我们仅仅解析了使用管道进行父进程与子进程之间的通信,但是在我们 shell 里面并不是这样的。 在 shell 里面执行 A | B 命令的时候,A 进程和 B 进程都是 shell 创建出来的子进程,A 和 B 之间不存在父子关系,它俩的父进程都是 shell

alt text

消息队列

消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核中的消息链表(内核是共享的),消息队列的消息体可以是用户自定义的数据类型。在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),当接收方接受数据时,也要与发送方的数据类型一样

缺点:

  • 通信不及时,因为每次数据的写入和读取都需要经过用户态和内核态的拷贝过程
  • 不适合大数据的传输,因为内核中的消息体都有一个最大长度的限制,同时所有队列包含的消息体也是有上限的

共享内存

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,大大提高了进程间通信的速度

优缺点:

  • 优点:不需要陷入内核态或者系统调用,也不需要拷贝数据
  • 缺点:多进程竞争同一个资源会造成数据的错乱

alt text

信号量

对于共享内存的多进程竞争资源,而造成数据错乱的问题。信号量可以解决

信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据

信号量表示资源的数据,有两种操作:

  • P操作:这个操作会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行
  • V操作:这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程
  • 总的来说,操作后:如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行

P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的

共享和互斥的实现方式:

  • 如果要使得两个进程互斥访问共享内存,我们可以初始化信号量为1
  • 如果要实现多进程同步的方式,我们可以初始化信号量为0

信号

信号跟信号量虽然名字相似度 66.66%,但两者用途完全不一样,就好像 Java 和 JavaScript 的区别

在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号:

alt text

运行在 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 编程模型

alt text

  • bind:服务端用于将把用于通信的地址和端口绑定到 socket 上
  • 这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket

针对 UDP 协议通信的 socket 编程模型

alt text

UDP 是没有连接的,所以不需要三次握手,也就不需要像 TCP 调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口号,因此也需要 bind

针对本地进程间通信的 socket 编程模型

本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是绑定一个本地文件,这也就是它们之间的最大区别