小林coding 《图解系统:进程管理》笔记

参考:

张杰上下文切换开销

Chris Terman:L17: Virtualizing the Processor

openCSF: 3.3. 管道和 FIFO 

 

进程

当进程要从硬盘读取数据时,CPU 不需要阻塞等待数据的返回,而是去执行另外的进程。当硬盘数据返回时,CPU 会收到个中断,于是 CPU 再继续运行这个进程。

虽然单核的 CPU 同时只能运行一个进程。但在 1 秒钟期间,它可能会交替运行多个进程,这样就产生并行的错觉,实际上这是并发。

 进程状态的变迁如下图:

 

  • 就绪态 -> 运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程;
  • 运行状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件;
  • 阻塞状态 -> 就绪状态:当进程要等待的事件完成时,由另一个进程唤醒,它从阻塞状态变到就绪状态;

物理内存空间是有限的,被阻塞状态的进程占用着物理内存就一种浪费物理内存的行为。

所以,在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运行的时候,再从硬盘换入到物理内存。

所以新加入一个挂起状态:来描述被从物理内存换出到磁盘的进程

另外,挂起状态可以分为两种:

  • 阻塞挂起状态:进程在阻塞状态时被换出到硬盘,等待事件唤醒后回到内存,回来后是就绪状态,等待被 CPU 调度
  • 就绪挂起状态:进程在就绪状态时被换出到硬盘,等待被调度后回到内存,回来后可以立即运行

 

进程的控制结构PCB

PCB 是进程存在的唯一标识linux 中的 task_struct,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。

  1. 进程描述信息:
    • 进程标识符:linux 中的 pid标识各个进程,每个进程都有一个并且唯一的标识符;
    • 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;
  2. 进程控制和管理信息:
    • 进程当前状态,如 new、ready、running、waiting 或 blocked 等;
    • 进程优先级进程抢占 CPU 时的优先级;
  3. 资源分配清单:
    • 有关内存地址空间或虚拟地址空间 (linux 中的 mm_struct 结构体) 的信息,所打开文件的列表和所使用的 I/O 设备信息。
  4. CPU 相关信息:
    • CPU 中各个寄存器、程序计数器 的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。

PCB 通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如:就绪队列,阻塞队列,运行队列

另外,对于运行队列在单核 CPU 系统中则只有一个链表了,因为单核 CPU 在某个时间,只能运行一个程序。

除了链接的组织方式,还有索引方式,不同状态对应不同的索引表,索引项指向 PCB。

一般会选择链表,因为可能面临进程创建,销毁等调度导致进程状态发生变化,所以链表能够更加灵活的插入和删除

 

进程的控制

创建进程:申请空白PCB;填写控制管理信息如进程唯一标志;将PCB插入就绪队列等待被调度

终止进程:查找要终止的进程的PCB;如果是运行态立即终止执行将CPU资源分配给其它进程;如果还有子进程则把这个孤儿进程交给1号进程接管;将进程拥有的全部资源归还系统;将其PCB在队列删除

阻塞进程:找到要阻塞的进程的PCB;如果是运行态则保护现场将其转为阻塞状态停止运行;将该PCB插到阻塞队列

唤醒进程:事件的相应阻塞队列找到PCB;将其在阻塞队列移出并置为就绪状态;将该PCB移到就绪队列;

 

进程的上下文切换

CPU 上下文

各个进程之间是共享 CPU 资源的,那么这个一个进程切换到另一个进程运行,称为进程的上下文切换。

操作系统将任务交给CPU运行前,操作系统需要事先帮 CPU 设置好 CPU 寄存器程序计数器,让 CPU 知道任务从哪里加载,又从哪里开始运行。所以说,CPU 寄存器和程序计数是 CPU 在运行任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下文

  • CPU 寄存器: CPU 内部一个容量小,但是速度极快的内存(缓存)。
  • 程序计数器:用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。

CPU 上下文切换就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

CPU 的上下文切换分三种:进程上下文切换、线程上下文切换、中断上下文切换

进程上下文切换

进程上下文切换主要涉及到两部分主要过程:

  • CPU 上下文切换:由进程 P1 切换到 P2 时,需要把当前 CPU 寄存器和程序计数器保存到 P1 中,然后 CPU 加载 P2 保存的 CPU寄存器和程序计数器
  • CPU TLB 页表失效:进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,一般采用的多级页表查找是一个很慢的过程,因此 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB ,通常称为页表缓存、快表等。由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后 TLB 就失效了Cache 失效导致命中率降低,那么虚拟地址转换为物理地址就要重新查询多级页表,表现出来的就是程序运行会变慢,而线程切换则不会导致 TLB 失效,因为线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。
  • CPU L1~L3 Cache 失效:CPU本身的L1、L2、L3级缓存,本身是为了加速数据读写、减少对内存的访问所引入的,如果任务切换后,任务需要读写的数据在CPU cache中没有找到,就要再去读写内存并同步到cache中去,这个是比较耗时间的。

 

在 CPU 芯片里面,封装了内存管理单元(Memory Management Unit)芯片,它的作用是讲 虚拟内存地址 转换为 物理内存地址,以及和 TLB 进行交互。

下图是 MMU 转换 虚拟内存地址的过程:

  • 首先,我们检查 TLB 是否缓存了所需的 虚拟内存地址 到 物理内存地址 的映射。
  • 如果没有,我们必须访问多级页表以查看该页面是否已分配了物理内存,如果是,则查找其物理页号。
  • 如果我们发现该页面未分配物理内存,则会向 CPU 发出缺页中断,以便它可以运行处理程序从物理内存分配该页面

 

一个进程最多可以创建多少个线程?

 

通过这里可以看出:

  • 32 位系统的内核空间占用 1G ,位于最高处,剩下的 3G用户空间;
  • 64 位系统的内核空间和用户空间都是 128T ,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的

可以创建多少线程和两个东西有关系:

  • 进程的虚拟内存用户空间上限,因为创建一个线程,操作系统需要为其分配一个栈空间,如果线程数量越多,所需的栈空间就要越大,那么虚拟内存就会占用的越多。
  • 系统参数限制,虽然 Linux 并没有内核参数来控制单个进程创建的最大线程个数,但是有系统级别的参数来控制整个系统的最大线程个数。
    •  /proc/sys/kernel/threads-max,默认值是 14553系统全局
    • /proc/sys/kernel/pid_max,默认值是 32768;表示系统全局的 PID 号数值的限制,每一个进程或线程都有 ID,ID 的值超过这个数就不行
    • /proc/sys/vm/max_map_count,默认值是 65530;表示限制一个进程可以拥有的VMA(虚拟内存区域)的数量

简单总结下:

  • 32 位系统,用户态的虚拟空间只有 3G,如果创建线程时每个线程分配的栈空间是 10M(也是系统参数 ulimit -a 的 stack size),那么一个进程最多只能创建 300 个左右的线程。
  • 64 位系统,用户态的虚拟空间大到有 128T理论上不会受虚拟内存大小的限制(28T/10M 个线程,1000多万个线程),而会受系统的参数或性能限制。

 

线程崩溃了,进程也会崩溃吗?

为什么 C/C++ 语言里,线程崩溃后,进程也会崩溃,而 Java 语言里却不会呢?

在进程中,各个线程地址空间是共享的,既然是共享,那么某个线程对地址的【非法访问】就会导致内存的不确定性,进而可能会影响到其他线程,这种操作是危险的,操作系统会认为这很可能导致一系列严重的后果,于是干脆让整个进程崩溃

线程崩溃后,会向进程发送一个信号(假设为 11,即 SIGSEGV,一般非法访问内存报的都是这个错误),进程没有注册自己的信号处理函数,那么操作系统会执行默认的信号处理程序(一般最后会让进程退出),但如果注册了,则会执行自己的信号处理函数,这样的话就给了进程一个垂死挣扎的机会。

  • Java 中常见的由于非法访问内存而产生的 Exception 或 error 是大家熟悉的 StackoverflowError 或者 NPE(NullPointerException)。StackoverflowError 是无限递归分配的栈内存超过了虚拟内存栈空间大小。
  • 因为 JVM 自定义了自己的信号处理函数,拦截了 SIGSEGV 信号,针对这两者不让它们崩溃。内部作了额外的处理:恢复了线程的执行,并抛出 StackoverflowError 和 NPE。

 

线程

线程与进程最大的区别在于:线程调度基本单位,而进程则是资源拥有基本单位

线程与进程的其它比较如下:

  • 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
  • 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;
  • 线程能减少并发执行的时间和空间开销

对于,线程相比进程能减少开销,体现在:

  • 线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
  • 线程的终止时间比进程快,因为线程释放的资源相比进程很多;
  • 同一个进程内的线程切换比进程切换快,原因在后面。
  • 由于同一进程的各线程间共享内存和文件资源,那么在同一进程的线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;

所以,不管是时间效率,还是空间效率线程比进程都要高。

线程的上下文切换

这还得看线程是不是属于同一个进程:

  • 当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
  • 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,TLB 缓存不会失效只需要进行 CPU寄存器、程序计数器 的硬件切换。

所以,线程的上下文切换相比进程,开销要小很多。

线程的实现

主要有三种线程的实现方式:

  • 用户线程User Thread):被称为1:N实现完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助
  • 内核线程Kernel Thread):被称为1:1实现。在内核中实现的线程,是由内核管理的线程;这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。
    • 轻量级进程LightWeight Process):程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程,轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间1:1 的关系称为一对一的线程模型。在大多数系统中,LWP与普通进程的区别也在于它只有一个最小的执行上下文调度程序所需的统计信息。

线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式,被称为 N:M实现。在这种混合实现下,既存在用户线程,也存在轻量级进程。 用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。

线程实现的优缺点:

用户线程的优点:

  • 用户线程切换时无需用户态与内核态的切换,所以速度特别快
  • 能够支持规模更大的线程数量

用户线程的缺点:

  • 线程的创建、销毁、切换和调度都是用户必须考虑的问题,而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至有些是不可能实现的。
  • 由于操作系统不参与线程的调度,如果一个线程发起了会阻塞的系统调用,内核无法将其它线程调度至处理器,那这个进程(1进程:1内核线程:N用户线程)所包含的用户线程都不能执行了
  • 由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会比较慢;
  • Java、Ruby等语言都曾经使 用过用户线程,最终又都放弃了使用它。但是近年来许多新的、以高并发为卖点的编程语言又普遍支 持了用户线程,譬如Golang、Erlang等,使得用户线程的使用率有所回升。

内核线程的优点:

  • 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行;
  • 分配给线程,多线程的进程获得更多的 CPU 运行时间;

内核线程的缺点:

  • 在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息,如 PCB 和 TCB;
  • 线程的创建、终止和切换都是通过系统调用的方式来进行,因此对于系统来说,系统开销比较大;

 

 

进程间通信方式(IPC)

IPC 一般有两种实现,POSIX 和 System V。二者基本概念相同,只是接口不同。涵盖了所有 IPC 机制:即管道、命名管道、消息队列、信号、信号量和共享内存

相对的IPC的持续性(Persistence of IPC Object)也有三种:

  1. 随进程持续的(Process-Persistent IPC)

    IPC对象一直存在,直到最后拥有他的进程被关闭为止,典型的随进程持久的IPC有pipes(管道)和FIFOs(先进先出对象)

  2. 随内核持续的(Kernel-persistent IPC)

    IPC对象一直存在直到内核被重启或者对象被显式关闭为止,在Unix中这种对象有 【System v 消息队列,信号量,共享内存】。(注意Posix消息队列,信号量和共享内存被要求为至少是内核持续的,但是也有可能是文件持续的,这样看系统的具体实现)。

  3. 随文件系统持续的(FileSystem-persistent IPC)

    除非IPC对象被显式删除,否则IPC对象会一直保持(即使内核才重启了也是会留着的)。如果 【Posix 消息队列信号量,和共享内存】都是用内存映射文件的方法,那么这些IPC都有着这样的属性。

 

1.管道

Unix IPC 的一种形式。管道允许进程使用单向字节流进行通信,两端由不同的文件描述符指定。管道的常见视觉类比是现实世界中的水管。倒入管子一端的水从另一端流出。

  • 管道是单向必须将一端指定为读取端,另一端指定为写入端。请注意,没有限制不同的进程必须从管道读取和写入管道;相反,如果一个进程写入管道然后立即从中读取,则该进程将收到自己的消息。如果两个进程需要双向来回交换消息,它们应该使用两个管道。
  • 管道是FIFO从管道接收端读取的所有数据都将与其写入管道的顺序匹配。无法随机查找,或指定为更高优先级以确保首先读取它。
  • 写入时 阻塞等待被消费。因此写入时一般都需要一个消费者正在等待读。
  • 容量有限

  • 对于匿名管道,它的通信范围是存在父子关系的进程。因为管道没有实体,也就是没有显式的管道文件,只能通过 fork 来复制父进程 fd 文件描述符,来达到通信的目的。
  • 对于命名管道,又叫FIFO。它可以在不相关的进程间也能相互通信。因为命令管道,提前创建了一个类型为管道的文件但是只存在于内存中),在进程里只要使用这个设备文件,就可以相互通信。

进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。

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

int pipe(int fd[2])

创建命名管道FIFO文件的系统调用。mode 是指定这个文件的权限

int mkfifo(const char *pathname, mode_t mode); 

匿名管道 - fork()

使用 fork 创建子进程时,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个「 fd[0] 与 fd[1]」,两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了。

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

    • 父进程关闭读取的 fd[0],只保留写入的 fd[1];
    • 子进程关闭写入的 fd[1],只保留读取的 fd[0];

即只保留 父进程单向向子进程传递数据,如果要支持双向通信,就要至少有两个管道

匿名管道 - shell 命令 |

管道最常见的用途之一是将命令行上的多个命令链接在一起。例如,考虑以下命令行:

首先,该ls命令打印出文件列表及其详细信息。该列表作为输入发送到sort,它根据第五个字段(文件大小)进行数字排序。然后,该tail进程获取最后一行,这是最大文件的行。最后,awk将打印该行的最后一个字段,这是最大的文件的文件名。

$ ls -l | sort -n -k 5 | tail -n 1 | awk '{print $NF}'

这四个进程都是由 shell 父进程 使用 fork()创建的子进程创建进程后,bash通过设置管道将每个进程与其后面的进程连接起来,链接其标准输入和输出。(这就是竖线 ( |) 被称为管道的原因。)

命名管道 (FIFO)

通过 mkfifo 命令来创建,并且指定管道名字:

$ mkfifo myPipe

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

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

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

$ echo "hello" > myPipe  // 将数据写进管道
                         // 停住了 ...

你操作了后,你会发现命令执行后就停在这了,这是因为管道里的内容没有被读取,只有当管道里的数据被读完后,命令才可以正常退出。

于是,我们执行另外一个命令来读取这个管道里的数据:

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

可以看到,管道里的内容被读取出来了,并打印在了终端上,另外一方面,echo 那个命令也正常退出了。

 

2.消息队列

消息队列是保存在内核中消息链表

消息队列相比管道,改进的地方在:

  • 消息队列允许一个或多个进程向它写入或读取消息。
  • 消息队列可以实现消息的随机查询,也可以指定优先级。不一定非要以先进先出的次序读取消息,也可以按消息的类型读取。比有名管道的先进先出原则更有优势。
  • 对于消息队列来说,在某个进程往一个队列写入消息之前,并不需要另一个进程在该消息队列上等待消息的到达读取。而对于管道来说,除非读进程已存在,否则先有写进程进行写入操作是没有意义的。
  • 消息队列的生命周期随内核(POSIX 可随文件),如果没有释放消息队列或者没有关闭操作系统,消息队列就会一直存在。而匿名管道随进程的创建而建立,随进程的结束而销毁。

不足之处:

  • 因为消息保存在内核之中,所以消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。
  • 消息队列不适合比较大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。在 Linux 内核中,会有两个宏定义 MSGMAX 和 MSGMNB,它们以字节为单位,分别定义了一条消息的最大长度一个队列的最大长度。

 

3.共享内存

消息队列的读取和写入的过程,都会有发生用户态与内核态之间的消息拷贝过程。那共享内存的方式,就很好的解决了这一问题。

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

  • 文件映射:映射物理内存中 Page Cache 中的 page(本来是由内核空间管理的物理内存,由磁盘数据缓存而来,用户空间映射到这块物理内存后,可由用户进程直接读写),对应于磁盘上的若干数据块(文件);对于这些页最大的问题是脏页回盘;
  • 匿名映射:映射在磁盘上没有对应文件的物理内存,没有实际载体。此类映射是为用户空间的栈和堆隐式创建的(可以由上面的图看出,栈、堆、BSS 段都是隐式的匿名映射区),或者如 malloc 通过显式调用 mmap(2) 系统调用来创建匿名映射区。

私有映射和共享映射

  • 共享文件映射:POSIX 共享内存
  • 私有文件映射:对于私有映射,进程A的修改对进程B是不可见的,利用的是 Copy On Write(COW)机制。
  • 共享匿名映射:一个典型应用是作为进程间通信机制的 Sytem V/POSIX共享内存。在Linux中,POSIX共享内存是通过挂载在/dev/shm下的tmpfs内存文件系统实现的,创建的每一个共享内存都对应tmpfs中的一个文件,因此POSIX共享内存也可视为共享文件映射(可以是内存持久也可以是文件持久)。
  • 私有匿名映射:最常见的用途是进程的堆段和栈段。以及glibc中的 malloc() 大于128k 时的内存分配。

用了共享内存通信方式,带来新的问题,那就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。

 

4.信号量(java sychronized monitor 底层)

为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,信号量就实现了对共享资源的保护机制

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

信号量表示资源的数量,控制信号量的方式有两种原子操作:

  • 一个是 P 操作进入共享资源之前会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
  • 另一个是 V 操作离开共享资源之后这个操作会把信号量加上 1相加后如果信号量 <= 0则表明当前有阻塞中的进程,于是唤醒阻塞的进程相加后如果信号量 > 0,则表明当前没有阻塞中的进程;

信号初始化为 1就代表着是互斥信号量,它可以保证共享内存在任何时刻只有一个进程在访问,这就很好的保护了共享内存。

信号初始化为 0,就代表着是同步信号量,它可以保证进程 A (只生产V+)应在进程 B (只消费P-)之前执行(A必须先生产B才会有数据消费;B先执行的话:A还没生产B就会阻塞等待,A生产完了唤醒B)。

 

5.信号

上面说的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。

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

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

$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

运行在 shell 终端的进程,我们可以通过键盘输入某些组合键的时候,给进程发送信号。例如

  • Ctrl+C 产生 SIGINT 信号,表示终止该进程;
  • Ctrl+Z 产生 SIGTSTP 信号,表示停止该进程,但还未结束;

如果进程在后台运行,可以通过 kill 命令的方式给进程发送信号,但前提需要知道运行中的进程 PID 号,例如:

  • kill -9 1050 ,表示给 PID 为 1050 的进程发送 SIGKILL 信号,用来立即结束该进程;

所以,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。

信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式:

1.执行默认操作。(比如  SIGTERM 信号终止进程) 2.捕捉信号,注册自己的信号处理函数。 3.忽略信号。

 

6.Socket

前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。

Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。

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

 

那线程通信间的方式呢?

同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,比如全局变量,所以对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题,信号量也同样可以在线程间实现互斥与同步(java sychronized monitor 底层)

  • 互斥的方式,可保证任意时刻只有一个线程访问共享资源;
  • 同步的方式,可保证线程 A 应在线程 B 之前执行;