← 博客

Process

进程和线程的区别

  • 进程:操作系统资源分配的最小单位,拥有独立的地址空间、文件描述符等。
  • 线程:CPU 调度的最小单位,共享进程的资源,包括代码段、数据段、堆等。线程不共享栈空间。

PCB

  • 操作系统每创建一个进程,都会创建一个 process control block,里面存储进程的 PID 和信息
  • process name
  • procee status(ready/blocked/running)
  • pc, stack pointer, file descriptors
  • process对资源(CPU 时间、内存空间)的需求情况

线程模型

UT:用户线程
KT:内核线程

1:1 模型(内核级线程)

  • java.lang.Thread, std::thread, pthread
  • 多核并行,资源利用率高
  • 适合 CPU 密集型任务

M:1

  • Python gevent &协程。一个内核线程负责多个用户线程
  • 一个用户线程的栈只占用几 KB 内存
  • 切换开销低,用户态调度不需要陷入内核。内核以为这个 KT一直在执行,没有切换过
  • 适合IO 密集型任务

M:N

  • gorountine: 多核 + 轻量,兼顾并行与并发

协程与线程的区别是什么?协程的优势和适用场景是什么?

  • 它的创建、调度、销毁都在 用户态 完成,不需要操作系统内核的参与
  • 栈通常只有几 KB,切换开销小
  • 协程的执行需要主动让出控制权(通常通过 yield 或 await),才能让其他协程运行。这一点与线程的抢占式调度不同。
  • JS和 Python 中async/await是一种协程的实现
  • 同一线程内的协程是串行执行的,无法并行

Process

进程独有的:

  1. PC
  2. stack pointer(存储返回地址)
  3. address space
  4. file descriptors

想要去 manipulate一个 process,我们必须交给内核态去做这件事,也就是在用户程序中调用 syscall。对于每个syscall,我们都必须检查它的 return value,如果为-1,则说明出错了。

进程的状态

  • new(PCB 未被分配)
  • ready
  • running
  • waiting
  • terminated

running->ready: time slice 用完了正常切换/更高优先级的 process is ready
running->waiting: 被阻塞

内核空间

  1. 内核空间位于整个地址空间的顶部,是所有用户进程共有的。用户进程的地址空间处于地址空间的底部。
  2. 只要系统在运行,内核就始终处于活动状态。它不像其他进程那样启动和停止;而是在系统启动时初始化,并持续运行直到系统关闭。

进程的终止

终止一个进程有三种方法:

  1. 收到了default action为terminate的signal
  2. 从main方法返回
  3. 显式call exit

fork()

Alt text 在fork()创建子进程时,子进程会继承父进程的一切,包括PC,stack pointer, fd; 子进程拥有和父进程一模一样,但是分开的虚拟地址空间,并且这两个虚拟地址空间指向同一块物理内存。因为父进程把 page table复制给子进程。

写时复制:父子进程拥有的同一块物理内存,在 fork()初期被设为 read-only. 当父进程或子进程尝试修改内存,会发生 page fault, 也叫写保护中断。 此时,OS 会给该进程复制一份新的内存页,然后修改应用于新的内存页上。

僵尸进程

子进程在执行完成时,会把它的return value放在某个地方,然后通知 OS 它完成工作了。此时父进程可以通过 wait()/waitpid()来获得子进程的 return value。

如果子进程已经 terminate,但是父进程没有通过wait()/waitpid()来获得退出状态,那么子进程仍然会消耗系统资源。比如OS需要存储它的 return value,也需要在process table 中留着它的 pid.

孤儿进程

如果父进程 terminate 了,它的子进程还没有 terminate,那么所有未完成的子进程被OS分配给init进程(pid=1)。init进程会负责在它们 terminate 之后,回收它们的资源。

文件描述符

在内存中,每个进程都有一个PCB,PCB中存在每个进程独有的file descriptor table,文件描述符就是这个table中的索引。

table 中存放指向资源的指针,比如指向heap中的一个 FILE 结构,这个 FILE 结构又指向磁盘中的".txt"文件。

进程间通信方式

管道

  1. 匿名管道:允许亲缘进程通信。
1
cat xx.txt | grep -n 'xxx'

shell先用fork()创建一个进程,并把它替换为cat进程;再对 grep做同样的事。

shell接着创建一个管道(内核中的一串缓存),允许cat的输出作为 grep的输入。

  1. 命名管道
1
mkfifo("/tmp/myfifo") #创建FIFO文件

相当于在磁盘中创建了一个名为myfifo的文件,两个进程都 open()这个文件,通过文件描述符实现读写,不过只允许单向传输

消息队列

消息队列是内核中的链表结构,进程按 “消息” 为单位发送数据,每个消息有固定格式(类型 + 数据),接收方可以按类型筛选消息。

优点:可以异步通信,数据结构化
缺点:占用内存多

系统调用:用 msgget 函数创建消息队列,再用 msgsnd 发消息,msgrcv 收消息。

共享内存

shmget创建共享内存。让不同进程的虚拟地址空间映射到同一块物理内存,这样进程间就能直接读写这块区域来交换数据,效率最高

优点:读写效率高
缺点:需要实现互斥,避免并发问题

信号量

实现进程的同步/互斥,不传递数据

同步:信号量初始化为0,代表资源数
互斥:信号量初始化为1

信号

有同步信号和异步信号,可以为信号定义一个捕捉函数,或者可以忽略。
SIGKILL无法被捕获或忽略

异步信号

  • 编号2-SIGINT-键盘中断(异步信号(中断)),由外部事件触发,与当前指令执行无关
  • 编号9-SIGKILL-强制终止

同步信号

  • SIGTRAP(调试断点),traps(有意触发的异常)
  • SIGSEGV(段错误),faults,由无效内存访问或非法操作码引起,unintentional but recoverable

线程之间同步方式