基本概念
状态、地址空间
- 三种基本状态 —— 就绪、运行、阻塞
-
进程控制块PCB(Process Control Block)
- 进程描述信息(如PID);
- 进程控制&管理信息(状态、优先级等);
- 源分配清单(地址空间状况、fd等);
- 处理其相关信息(各寄存器的值等)
进程存在的标识,在Linux系统中是task_struct,task_struct在内核栈(Linux进程氛围用户栈和内核栈)的尾端分配。
- 进程地址空间
从低地址到高地址:
- text 代码段 —— 代码段,一般是只读的区域;
- static_data 段 =
- stack 栈区 —— 局部变量,函数的参数,返回值等,由编译器自动分配释放;
- heap 堆区 —— 动态内存分配,由程序员分配释放;
- 进程与线程
进程是拥有资源的基本单位,进程的地址空间相互独立;
线程是独立调度的基本单位,共享同一个进程内的资源(线程有自己的栈),减少了程序并发时所付出的时空开销,并且可以高效的共享数据,有效地利用多处理器和多核计算机,提高os的并发度。
一个进程异常退出不会引起另外的进程运行异常;但是线程若异常退出一般是会引起整个进程奔溃。
创建/撤销/切换 进程的开销远大于线程的(创建线程比创建进程快10~100倍 UNPv2/P406)。
- 僵尸、孤儿、守护
孤儿进程 —— 父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程 —— 一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。
守护进程 —— 守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。
最大进程数以及单进程内的最大线程数?
最大进程数受以下3方面限制:
- 不能超过pid_t类型的最大值
- 使用命令ulimit -u查看系统中限制的最大进程数。/etc/security/limits.conf里面是硬限制,ulimit -u是软限制,内核参数kernel.pid_max也做了限制。
- 受系统资源限制,创建一个新进程会消耗系统资源,最主要的就是内存。
IPC (interprocess communication)
UNP 分了以下几中形式的IPC:
- 消息传递 —— 管道、FIFO、消息队列
- 共享内存
- 同步 ——信号量、互斥量、条件变量、读写锁、文件和记录锁、
- 远程过程调用 —— solaris 门、Sun RPC
- 跨网络 IPC —— 套接字
- 域套接字
- 信号
进程间通信方式:匿名管道,有名管道,消息队列,共享内存,信号量,套接字,域套接字,信号
线程同步方式:互斥量,条件变量,读写锁
进程间通信
匿名管道
半双工的(即数据只能在一个方向上流动),具有固定的读端和写端;是一种特殊的文件(pipefs,挂载在内核中),有固定大小,只存在于内存中;
实现原理:
管道是由内核管理的一个缓冲区,被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。
在 Linux 中,管道的实现借助了文件系统的file结构和VFS的索引节点inode。通过将两个file结构指向同一个临时的VFS索引节点,而这个VFS索引节点又指向一个物理页面而实现的。
内核会利用一定的机制同步对管道的访问。
有名管道
半双工,可在无关进程使用;FIFO有路径名与之相关联,以一种特殊设备文件形式存在于文件系统中
实现原理:
Linux中设立了一个专门的特殊文件系统--管道文件,FIFO在文件系统中有对应的路径。当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,虽然FIFO在VFS的目录树下可见,但是它并不对应disk上的文件。
本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统来为管道命名。当删除FIFO文件时,管道连接也随之消失。当进程终止时,管道内的数据会被删除。
消息队列
- 面向记录的,其中的消息具有特定的格式以及特定的优先级;
- 独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除(随内核的持性)。
- 可以实现消息的随机查询,不一定要以先进先出的次序读取,也可以按消息的类型读取。
为什么不使用消息队列
- 进程终止时,消息队列及其内容并不会被删除(随内核的持性)。
- 在文件系统中没有名字,不能使用IO。
共享内存和信号量
共享内存是最快的:
通常往管道、FIFO或消息队列写入数据时,这些IPC需要将数据从进程复制到内核,通常总共需要复制4次,而共享内存则只拷贝2次数据;如图:
信号(Signal)
用于通知接收进程,有某种事件发生。
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达。
- SIGRTMIN之前的信号是非排队(不可靠)的,多次连续发生的相同信号,只递交一次;SIGRTMIN之后的信号不会丢失,会递交多次。
- 在信号处理函数
- singal:只阻塞当前正在处理的信号,后续信号会排队(也会区分可靠和不可靠)
- sigaction:可以设置阻塞正在处理的信号和其他型号
Pipe 与 FIFO 比较:
- pipe在特殊文件系统pipefs中(内核中),VFS 目录树下不可见;FIFO 在目录树下可见;
- 都有 inode,但没有磁盘镜像(disk image);
- Pipe 用于亲缘关系进程通信;FIFO 无此要求;
- 限制都包含两个:OPEN_MAX(一个进程在任意时刻打开的最大描述符,默认1024);还有PIPE_BUF(可原子性地往一个管道/FIFO 的最大数据量,默认 4K)
线程间同步
互斥锁(Mutex)
加锁原语,排他性访问共享数据,用于保护临界区。可细分为递归锁/非递归锁。
如果存在某个线程依然使用原先的程序
(即不尝试获得mutex,而直接修改共享变量),互斥锁不能阻止其修改。所以,互斥锁机制需要程序员自己来写出完善的程序来实现互斥锁的功能(以下锁 一样)。
条件变量(Condition Variable)
互斥锁用于上锁,条件变量用于等待,条件变量的使用是与互斥锁共通使用的。
条件变量学名叫管程(monitor)【From muduo P40】。
读写锁
读写锁也叫做 共享-独占锁,允许更高的并发度。
互斥量要么是锁住状态,要么是不加锁状态,而且一次只有一个线程对其加锁。
读写锁可以有三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可用同时占有读模式的读写锁。
读写锁可以通过使用互斥锁和条件变量来实现。
自旋锁(spinlock)
在获取锁之前一直处于忙等(自旋)阻塞状态;常用于
锁被持有的时间短,且线程并不希望在重新调度上花费太多成本。当线程自旋等待锁变为可用时,CPU不能做其他事情。
故而自旋锁常作为底层原语,用于实现其他类型的锁。
记录锁
记录锁是读写锁的一种扩展类型,可用于亲缘关系或无亲缘关系的进程之间共享某个文件的读与写。被锁住的文件通过文件描述符进行访问,执行上锁的操作函数是fcntl,这种类型的锁通常在内核中维护。
记录锁的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区,即其锁定的是文件的一个区域或整个文件。
记录锁有两种类型:共享读锁,独占写锁。基本规则是:多个进程在一个给定的字节上可以有一把共享的读锁,但在一个给定字节上的写锁只能有一个进程独用。即:如果在一个给定的字节上已经有一把读或多把读锁,则不能在该字节上再加写锁;如果在一个字节上已经有一把独占性的写锁,则不能再对它加任何读锁。
死锁
- 死锁 —— 就是两个或多个进程被无限期地阻塞、相互等待的一种状态。
- 死锁的四个条件
- 竞争同一个资源
- 持有资源不释放
- 不能抢占资源
- 循环使用资源
处理死锁的策略:
一般来说,打破循环使用资源最容易,即顺序加减锁
银行家算法(死锁避免算法)。在资源动态分配过程中,防止系统进入不安全状态,以避免发生死锁。
数据库中会用到等待图进行死锁检测
- 死锁定理:
通过将资源分配图简化的方法,来检测系统状态是否为死锁状态。
当且仅当S状态的资源分配图不可完全简化时,S为死锁状态。
经典问题
- 生产者-消费者问题
- 读者-写者问题
Futex
在Linux下,信号量和线程互斥锁的实现都是通过futex系统调用。