进程

进程是正在运行的程序实例,包括程序计数器,寄存器和变量当前值

进程的内部

虚拟地址空间+至少一个控制线程 空间的分段: r-x是程序段,r是定义的常量,rw是定义的变量存放的段 rw其中又可以分为栈堆空间,malloc 的内存也是放在这里

线程

线程是CPU被调度执行的实体,线程中有程序计数器,拥有寄存器,有自己的堆栈。进程只是用于把资源集中在一起。多个进程共享物理内存,磁盘等资源,多个线程共享同一个地址空间和其他资源

线程仅仅被视为一个与其他进程共享某些资源的进程,而是否共享地址空间几乎是进程和 Linux 中所谓线程的唯一区别

多个子进程与多线程的区别:多线程之间拥有相同的地址空间,可以相互访问没有保护。多进程有各自的地址空间

用户级线程

需要有统一的运行时用专门的线程表来管理每个线程,与内核记录进程的表差不多

进程的生命周期

创建:通过系统调用或者系统初始化的时候 守护进程:在后台处理活动的进程 终止条件:正常退出,出错退出,严重错误,被杀死 状态:运行,阻塞,就绪

进程的管理

操作系统会维护一个结构体数组叫进程表,每一个项叫做进程控制块,里面保存了进程暂停和恢复或需要的状态信息,假如发生中断(例如IO),那么中断硬件会将程序计数器,状态字还有一个或多个寄存器压入堆栈,然后交给中断服务例程处理剩下工作,寄存器会被优先保存,然后堆栈指针会指向一个临时的空间供例程使用,结束后会运行调度程序,由调度程序决定从就绪队列中选择进程来执行

进程的系统调用

fork()

fork 复制了什么? 调用线程,进程的地址空间,进程的文件描述符表,数据段和代码段 采用写时复制技术,物理上还是映射到父的物理空间 没有复制的:文件句柄 因此 fork 之后的子进程和父进程的符表指向相同的文件句柄 返回值 0 – 子进程 返回值 PID – 父进程 复制之后,父子之间只共享句柄

vfork()

vfork设计用以子进程创建后立即执行execve系统调用加载新程序的情形。在子进程退出或开始新程序之前,内核保证了父进程处于阻塞状态(处于挂起的状态),保证了子进程优先执行 vfork 创建出来的子进程共享了父进程的所有内存以及数据段,可以用来减少 fork 中不必要的拷贝

clone()

可以将资源有选择的复制给子进程,没有复制的数据结构通过指针的复制共享。一般用作参数传递来创建线程

僵尸进程

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中

内核在进程退出的时候会对其资源进行释放,并保留一些信息,直到父进程来取这些信息的时候才会开始释放

如何防止堆积的僵尸进程占用过多的系统资源呢?

  1. 当子进程调用 exit() 的时候,父进程会收到 SIGCHILD 信号提示可以使用 wait() 来检索子进程的错误码
  2. 循环调用 waitpid() 来等待所有的退出子进程,通常不会使调用者陷入阻塞,而 wait () 如果没有找到退出的进程,就会使调用者进入阻塞状态
  3. 干掉父亲,让 init 1 号进程来处理

孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由 init 进程通过 wait 对它们完成状态收集工作,因此孤儿进程不会有什么危害

进程间通信

  1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系,实质是内核缓冲区
  2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信,磁盘上有 inode 无数据块
  3. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点,可以自定义指定接收特定的消息类型
  4. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。 实现:
    • mmap 映射同一个普通文件,文件就变成了内存,不需要进行文件操作来传递数据
    • UNIX system V 通过映射一个特殊文件系统,通过标准 key 取获取共享内存 ID,根据 ID 映射共享内存
  5. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  6. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
  7. 信号 ( signal ):信号是一种比较复杂的通信方式,是进程间通信机制中唯一的异步通信机制。用于通知接收进程某个事件已经发生

机器级的原子指令: TSL(Test and Set Lock)执行此指令的 CPU 会锁住内存总线,禁止其他CPU在指令结束前访问内存,离开的进程重新 mov 赋值即可释放锁 XCHG 然后 CMP CAS操作实现使用 CMPXCHG 汇编指令 都有忙等待的缺点,可以使用 sleep 和 wakeup 系统调用阻塞挂起和唤醒进程 使用信号量进行同步,P信号量-1,V信号量+1

条件变量:pthread_cond_wait,pthread_cond_signal,常用于线程的相互等待,比如在生产者消费者问题中,wait 直到有其他的线程给其发信号,有多个线程等待用一个信号的时候可以使用 pthread_cond_broadcast 条件变量常常与互斥量一同使用,这种模式让一个线程锁住一个互斥量,没有得到期望的结果时等待一个条件变量,这使得阻塞可以变成原子操作。 如果没有进程在等待条件变量,那么signal 信号会永远消失,因此 wait 必须在 signal 前

管程:高级同步原语,任意时刻管程中只能有一个活跃进程,互斥由编译器完成(go中直接由通道数据结构负责),如果管程中有其他进程,调用进程会被挂起。假如缓冲区满了,可以配合条件变量进行阻塞,消费者可以通过对其伙伴等待的条件变量执行 signal

屏障:用来同步进程组,除非所有进程就绪准备着手下一个阶段,否则任何进程都不能进入下一个阶段

管道:日常在终端执行shell命令时,会大量用到管道。但管道的缺陷在于只能在有亲缘关系(有共同的祖先)的进程之间使用,反正只要共同的祖先调用了pipe函数,打开的管道文件就会在fork之后,被各个后代所共享

重定向:通过 dup2 将文件描述符进行替换,比如 dup(4, 1) 就是将 fd4 的描述符指向的文件作为 fd1 的指向,往 fd1 写的时候,调用者不会知道具体的目标文件是什么

死锁

条件:

  1. 资源互斥
  2. 占有和等待
  3. 不可抢占
  4. 环路等待

检测:资源分配图包含一个或一个以上的环;向量比较 恢复:

  1. 利用抢占恢复
  2. 回滚恢复:设置检查点
  3. 杀死进程

避免:资源轨迹图

进程的调度时机

  1. 进程的创建与退出
  2. 进程阻塞在IO或者信号量或其他原因阻塞时
  3. IO中断发生时

调度算法

先来先服务,最短作业优先,最短剩余时间(抢占),轮转,优先级

线程调度:

  1. 用户级线程:切换开销小,内核不可知
  2. 内核级线程:切换开销大,不会因为线程阻塞IO导致整个进程被挂起

Linux调度算法:动态优先级,奖励互动进程,惩罚占用CPU的进程