进程与线程 | Personal Blog

进程与线程

操作系统由壳与核组成,壳负责用户交互;内核负责对资源的管理

进程

进程就是正在执行程序的实例

CPU在多个运行的进程之间不断切换,在宏观看来,每个进程都分配到一个虚拟CPU

进程创建后,父进程与子进程有不同的地址空间,在一个进程中修改的数据不会影响另一个进程

进程的组成

程序段

程序段用来存放程序员编写好的程序

数据段

数据段用来存放程序运行时需要访问储存保护的数据

进程控制块(PCB)

  1. 程序计数器:接着要运行的指令地址

  2. 进程状态:可以是new、ready、running、waiting或 blocked等

  3. CPU暂存器:如累加器、索引暂存器(Index register)、堆栈指针以及一般用途暂存器、状况代码等,主要用途在于中断时暂时存储数据,以便稍后继续利用;其数量及类因电脑架构有所差异

  4. CPU排班法:优先级、排班队列等指针以及其他参数

  5. 存储器管理:如标签页表等

  6. 会计信息:如CPU与实际时间之使用数量、时限、账号、工作与进程序号

  7. 输入输出状态:配置进程使用I/O设备,如磁带机

进程的创建

创建的原因

  1. 系统初始化: 启动操作系统时,通常会创建若干进程,以完成所需的任务

  2. 正在运行的程序执行了fork系统调用:fork被调用一次,返回两次;子进程中返回零,父进程返回子进程进程id;此子进程与父进程通过写时复制共享内存;子进程与父进程相互独立;一般情况下,创建子进程的目的不是执行与父进程相同的程序,此时需要在调用fork函数后调用exec函数族指定需要运行的程序

  3. 用户请求创建一个新进程: 在某些系统中,用户能够同时开启多个进程,并与某进程进行交互

创建的流程

  1. 申请空白PCB

  2. 分配资源:包括物理和逻辑资源;内存、文件、CPU时间等

  3. 初始化PCB:填入标识符;设置程序计数器;初始化进程状态

  4. 就绪

进程的终止

终止的原因

  • 正常退出

  • 出错退出

  • 严重错误

  • 被杀死

终止的流程

  1. 根据被终止进程的标识符,检索PCB

  2. 终止进程

  3. 终止该进程的子孙进程

  4. 将该进程占有的资源归还父进程或系统

  5. 从相关表中移除PCB

进程的状态

  • 运行态:CPU空闲,没有被阻塞

  • 就绪态:CPU空闲,但CPU资源没有分配给进程

  • 阻塞态:由于输入或其他外部条件未就位,即使CPU空闲,进程也不能运行

  • 挂起态:为了对进程进行观察,需要将某一进程挂起

挂起、激活、阻塞与唤醒

OS通过挂起原语将指定进程挂起,挂起的发动者可以是系统、父进程或自己;挂起后进程从内存调度至磁盘

OS通过激活原语将被挂起的进程激活,激活的发动者可以是系统、父进程或用户,但不能是该进程本身,因为被挂起的进程在外存中,无法申请

阻塞是进程自身的主动行为,当外界条件不满足需要时,进行阻塞

当阻塞进程期待的资源等发生时,将唤醒进程,但进程不能唤醒自己,须由其他相关进程执行唤醒原语,否则进程将永远阻塞

线程

线程是计算机操作系统能够调用的最小单位,是进程的实际运作单位

当存在大量的计算或其他需要处理的任务时将不同的琐碎任务交由多个小的进程完成是有益的

线程在进程中运行,共享系统分配给进程的资源;也可以申请独占资源

每一个线程都拥有自己的堆栈,用以存储相应过程中产生的局部变量和执行历史等(上下文)

线程包括堆栈寄存器和线程控制块

线程控制块

线程控制块是操作系统内核中的一种数据结构,用来存放线程的相关信息

线程控制块TCB包括线程标识、线程状态、存储区(存储线程运行的上下文)、寄存器、堆栈指针与优先级等

在用户空间内实现线程

计算机拥有虚拟内存,将虚拟内存空间分为两部分,一部分划分给操作系统的内部核心程序,称为内核空间;另一部分划给各个进程,称为用户空间

在用户空间内调用各个线程,可以通过线程表迅速完成对线程的阻塞,启动或转换;由于这些操作都是在本地空间内完成的,不需要调用内核或是切换上下文,线程的调度十分快捷

用户级线程的相关信息存储在用户空间中的线程控制块中,不能直接进行系统调用,需要将资源要求提交给运行时系统(即管理控制线程函数的集合),再由运行时系统通过系统调用进行系统资源的分配

阻塞系统调用带来的问题:若一个线程在被调用的过程中被阻塞了,不能及时读取到需要的输入,就会影响乃至停止其他线程;为了防止这类情况出现,使用包装器检查该线程的调用过程中是否安全,若可能被阻塞,则不调用该线程

线程独占CPU带来的问题:由于没有时钟中断,当一个线程不放弃CPU的独占时,该进程的其他线程就不能够运行

内核分配的问题:每次内核只分配给用户一个CPU,只能同时运行一个线程

在内核中实现线程

内核中拥有记录所有线程的线程表,希望进行调用的线程通过系统调用对线程表进行更新完成线程的创建和撤销

内核可以同时调度同一进程的多个线程并行执行

当一个进程阻塞时,内核根据某种规则,选择运行另一个线程或不运行

内核级线程的信息存储在内核空间的线程控制块,由内核管理

在内核中实现的线程称为轻型进程LWP,用户级线程通过链接LWP实现与内核线程之间的连接;LWP作为缓存池,没有连接到缓存池的用户级线程阻塞,实现内核与用户级线程的隔离,使用户级线程上下文切换与内核无关

进程间通信

管道通信系统

管道是一个固定大小的缓冲区,通信双方通过管道完成数据的传输

管道需要提供:同步、互斥与验证存在的操作

管道根据在文件系统中是否可见分为具名管道及匿名管道

匿名管道文件在内核中linux中央文件系统VFS下,负责不同用户空间之间的通信

  • 匿名管道在shell中用管道符|表示,管道的作用在于将管道前的输出作为管道后的输入

  • 具名管道使用mkfifo函数创建,创建FIFO文件

匿名管道只能用于有亲缘关系的两个进程之间,而具名管道可以被用于任意两个能被文件管理系统访问的文件之间

客户机-服务器系统

  • 套接字是一个通信标识类型的数据结构、以套接字标识符区分
  1. 通信目的地址
  2. 通信使用的端口号
  3. 通信网段的传输层协议
  4. 进程的网络地址
  5. 系统调用
  • 基于文件的套接字:一个套接字与一个特殊文件绑定,通信双方对此文件进行读写操作完成通信、

  • 基于网络的套接字:一对套接字分属两个进程,与端口绑定

  • RPC:远程过程调用 ,不允许不同主机的进程互相调用

  • 存根:每一个独立运行的远程过程都有一个存根与之相关联 进行远程操作时,调用存根对数据进行操作 存根将部分相关信息打包上传,并附上请求传递给服务器过程 服务器进程存根接收服务器进程传递的信息并执行打包回传结果

竞争条件

不同的进程对同一资源进行修改,由于时钟中断和CPU调用时序,可能不同的进程对同一份变量的认识是不一样的

临界区

为了避免竞争条件,在涉及共享内容时,需要设计临界区以避免多个进程对同一个共享内存进行读写操作

当一个进程进入了临界区时,其他试图进入临界区的进程就会被挂起直至该进程离开临界区

忙等待的互斥

  • 屏蔽中断:一个进程进入临界区后立即屏蔽所有中断,离开前再打开中断,这样可以避免其他进程打扰;但系统将屏蔽中断的权利下放给用户,这是不明智的,因为恶意程序很有可能不再打开中断;屏蔽中断的操作更适合操作系统而不是用户

睡眠与唤醒

对于生产者-消费者问题,我们规定:当缓冲区的信息满时,生产者睡眠,否则生产者向缓冲区放入信息;当缓冲区的信息空时,消费者睡眠,否则消费者从缓冲区取出信息

意外情况:消费者发现缓冲区空,决定睡眠但此时调度程序决定暂停消费者的动作而开始运行生产者;生产者放入信息,逻辑上认为消费者在睡眠状态,决定唤醒消费者;但消费者逻辑上未睡眠,于是测试原先读取并存放在堆栈中的缓冲区的值,认为缓冲区为空,决定继续睡眠;此时生产者仍在不断地向缓冲区放入信息,一旦缓冲区满,生产者也将睡眠,系统死锁

信号量

信号量可以通过整型变量保存,P操作和V操作信号量表示资源的减一与增一

P操作表示当资源量小于等于0时,阻塞直至有资源释放;V操作则表示释放一个资源

当有多个进程在同一个信号量上睡眠时,系统随机挑选一个执行up操作,使该进程继续执行

P操作和V操作都是原子性的,不可分割的

同步操作在异步操作之前执行

每一个偏序关系使用一个信号量

  1. 整型信号量: 即PV操作

  2. 记录型信号量: 不仅记录信号量的增减,还记录进程的请求顺序

  3. AND型信号量: 当一个进程同时需要多个资源时,为了防止死锁,需要同时分配多个资源

互斥量

用一个整型量表示解锁和加锁,若互斥量是解锁的,调用的进程或线程则调用成功,自由进入临界区;若已加锁,调用线程被阻塞,直到锁被释放;但被阻塞的进程并不进行忙等待

管程

管程是由过程、变量及数据结构组成的一个集合,用少量的信息对某资源进行抽象描述

任意时刻,管程中只能有一个活跃的进程;进入管程的互斥行为由编译器执行

  • 条件变量:配合相关函数,使调用进程阻塞

使用管程则不必担忧出现sleep-wakeup的失败状况,因为管程中严格互斥,解决了共享资源的互斥使用问题

管程中的共享变量在管程外不可见,不能被外部访问

杂项

内核

内核是操作系统中最基本的部分,负责管理系统的进程、内存、设备驱动程序、文件和网络系统等等,决定着系统的性能和稳定性。

系统调用与函数

系统调用在内核态中进行
函数调用在用户态中进行

  1. 系统调用(内核态): 系统调用说的是操作系统提供给用户程序调用的一组特殊接口。用户程序可以通过这组特殊接口来获得操作系统内核提供的服务,比如用户可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件,可以通过时钟相关的系统调用获得系统时间或设置定时器等。
    控制硬件—系统调用往往作为硬件资源和用户空间的抽象接口,比如读写文件时用到的write/read调用。 设置系统状态或读取内核数据——因为系统调用是用户空间和内核的唯一通讯手段,所以用户设置系统状态,比如开/关某项内核服务(设置某个内核变量),或读取内核数据都必须通过系统调用。
    进程管理—系统调用接口是用来保证系统中进程能以多任务在虚拟内存环境下得以运行。比如 fork、clone、execve、exit等
    Linux中实现系统调用利用了0x86体系结构中的软件中断。首先,用户程序为系统调用设置参数。其中一个参数是系统调用编号。参数设置完成后,程序执行“系统调用”指令。
    新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。

  2. 函数调用(或库函数调用,用户态, 与平台无关但是最终或多或少依赖于系统调用) 函数调用就相对熟悉多了。我们大一、大二编程所用的函数调用大多都是这里的函数调用,它运行在用户空间。
    它主要通过压栈操作来进行函数调用。
    例如,函数调用会执行下列操作:
    ⒈ 将帧指针压入栈中:pushebp
    ⒉ 使得帧指针等于栈指针:movebp,esp
    ⒊ 使栈指针自减,自减得到的内存地址应当能够(足够)用来存储被调用函数的本地状态:sub esp,0CCh

fork与写时复制

写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只有在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
任何进程希望写入共享页,则内核会拷贝一份页面,并标记为可写,但原页面依然是写保护的,只有该页面的唯一属主才能进行写入
这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的情况下—例如,fork()后立即执行exec(),地址空间就无需被复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建一个进程描述符。在一般情况下,进程创建后都为马上运行一个可执行的文件,这种优化,可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。

exec

exec是一个函数族,可以根据指定的文件名找到可执行文件,并代替调用进程;

某进程调用exec函数族中某一个函数后,该进程的一切实体资源都被新的内容取代,包括数据段、代码段、堆栈;只有进程号等标识信息不变

重定向

Linux 中标准的输入设备默认指的是键盘,标准的输出设备默认指的是显示器。
输入重定向:指的是重新指定设备来代替键盘作为新的输入设备;
输出重定向:指的是重新指定设备来代替显示器作为新的输出设备。

文件描述符

文件描述符是一个非负整数,标识一个程序打开文件的序号,每用OPEN打开一个文件则会返回一个文件描述符;0标识标准输入,1标识标准输出,2标识错误输出

本文共5281字符