目录
进程、线程和协程的区别
线程和进程的区别
进程
线程
进程间通信方式
线程同步机制
守护进程、僵尸进程、孤儿进程
进程/线程切换过程切换的资源有哪些
进程、线程和协程的区别
进程 | 线程 | 协程 | |
定义 | 资源分配和拥有的基本单位 | 程序执行的基本单位 | 用户态的轻量级线程,线程内部调度的基本单位 |
切换情况 | 进程CPU环境(栈、寄存器、页表和文件句柄等)的保存以及新调度的进程CPU环境的设置 | 保存和设置程序计数器、少量寄存器和栈的内容 | 先将寄存器上下文和栈保存,等切换回来的时候再进行恢复 |
切换者 | 操作系统 | 操作系统 | 用户 |
切换过程 | 用户态->内核态->用户态 | 用户态->内核态->用户态 | 用户态(没有陷入内核) |
调用栈 | 内核栈 | 内核栈 | 用户栈 |
拥有资源 | CPU资源、内存资源、文件资源和句柄等 | 程序计数器、寄存器、栈和状态字 | 拥有自己的寄存器上下文和栈 |
并发性 | 不同进程之间切换实现并发,各自占有CPU实现并行 | 一个进程内部的多个线程并发执行 | 同一时间只能执行一个协程,而其他协程处于休眠状态,适合对任务进行分时处理 |
系统开销 | 切换虚拟地址空间,切换内核栈和硬件上下文,CPU高速缓存失效、页表切换,开销很大 | 切换时只需保存和设置少量寄存器内容,因此开销很小 | 直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快 |
通信方面 | 进程间通信需要借助操作系统 | 线程间可以直接读写进程数据段(如全局变量)来进行通信 | 共享内存、消息队列 |
线程和进程的区别
- 执行单元:进程是操作系统资源分配的基本单位,是一个完整的运行环境,
- 而线程则是任务调度和执行的基本单位。
- 资源分配和管理:
- 每个进程都有自己的独立的进程地址空间、系统资源和文件描述符表,
- 而线程共享进程的资源。但每个线程各自都有一套独立的寄存器和栈
- 因此,进程之间的切换需要一定的时间和开销,而线程之间的切换则相对快速和轻量级。
- 由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;
- 通信方式:
- 进程间通信需要特定的机制,如管道、消息队列、共享内存、信号量等。
- 同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,比如全局变量,所以对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题,信号量也同样可以在线程间实现互斥与同步:
- 安全性:
- 由于进程之间是相互隔离的,因此一个进程中的代码不会对其他进程产生影响
- 而线程之间则可能会相互干扰或者造成数据不一致等问题。一个线程的错误可能会导致整个进程崩溃。
- 从内核的角度来说,Linux把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是内存管理机制来处理线程,而是和进程一样,使用相同的调度算法和内存管理机制。
进程
进程是操作系统中的一个独立的执行单元,它包含了程序的代码、数据以及一些运行时的状态信息。每个进程都有自己的内存空间,独立于其他进程,使其能够互相隔离。
进程可以通过多种方式创建,包括通过shell命令启动程序、由其他进程派生(fork)新进程。新进程拥有自己的进程标识符(PID)。
操作系统对进程的管理方法是先描述再组织。创建一个进程实际上就是先将进程的代码和数据加载到内存上,接着操作系统对该进程进行描述形成对应的PCB(PCB是对进程控制块的统称),Linux下描述进程控制块的结构体叫做task_struct。在Linux的根目录下有proc文件,这个文件里面就包含大量的进程信息,可以使用ps命令来查看进程信息
进程在其生命周期中可以处于不同的状态,包括就绪态、运行态、等待态等。就绪态表示进程已准备好执行但尚未获得CPU时间片,运行态表示进程正在执行,等待态表示进程正在等待某个事件的发生。
操作系统负责调度进程以便它们可以共享CPU时间。进程调度算法决定了哪个进程获得CPU时间,通常使用抢占式调度算法。
进程可以通过多种方式进行通信,包括管道、套接字、消息队列、共享内存等。这些机制允许进程在不同的地址空间中传递数据和信息。
进程可以正常退出,也可以被强制终止。当进程完成其工作或者遇到错误时,它会释放其资源并终止。操作系统也可以通过发送信号来终止进程。
系统管理员可以使用工具来监控和管理系统中的进程,例如ps、top、kill等命令。这些工具允许管理员查看进程的状态、资源使用情况以及终止不需要的进程。
线程
线程是进程内的一个独立执行单元,它与同一进程中的其他线程共享相同的进程内存空间。线程包含了程序代码、数据以及一些运行时状态信息,因此线程比进程更轻量级,创建和切换开销较小,可以更有效地利用系统资源。但它们与进程内的其他线程共享相同的资源,如文件描述符和全局变量。
Linux下的线程是最小的执行单位,调度的基本单位。从内核的角度来说,Linux把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是内存管理机制来处理线程,而是和进程一样,使用相同的调度算法和内存管理机制。
线程有自己的生命周期,包括创建、就绪、运行、阻塞和终止。线程可以被创建、启动、暂停、恢复和终止。
多个线程共享进程内存,因此需要进行同步以避免数据竞争和冲突。常见的同步机制包括互斥锁、条件变量、信号量和读写锁等,这些机制用于控制线程对共享资源的访问。线程可能引入竞争条件和死锁,需要仔细设计和测试多线程应用。
操作系统负责线程的调度,以确定哪个线程获得CPU时间片执行。调度算法通常以优先级、时间片轮转等方式决定线程执行的顺序。
多线程应用程序对于用户输入和外部事件的响应更快,因为一个线程的阻塞不会影响其他线程的执行。
线程比进程更轻量级,创建和切换开销较小,可以更有效地利用系统资源。
进程间通信方式
进程间通信就是在不同进程之间传播或交换信息。进程间通信的本质就是,让不同的进程看到同一份资源。
匿名管道
shell 命令中的「|」竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,再来匿名管道是只能用于存在父子关系的进程间通信或者具有共同祖先的进程之间通信
- pipe 函数用于创建匿名管道
- 管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
命名管道
命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。通信数据都遵循先进先出原则。
- 可以使用 mkfifo 创建一个命名管道。
消息队列
消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。消息队列提供了一种异步通信的机制,所以通信速度不及时!
比如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。同理,B 进程要给 A 进程发送消息也是如此。
共享内存
共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问, 而无需数据复制和内核参与。享有最快的进程间通信方式之名。但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。
- 创建共享内存我们需要用 shmget 函数
信号量
那么,就需要信号量来保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作。
- 减减的操作就叫做 P 操作,而计数器加加的操作就叫做 V 操作,P操作就是申请信号量,而V操作就是释放信号量。
- P(等待)操作用于尝试获取资源或者等待某个条件成立,如果资源不可用或条件不成立,则会阻塞。
- V(发信号)操作用于释放资源或者通知条件成立,如果有其他进程正在等待,则唤醒其中一个。
信号是异步通信机制,一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SIGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。
Socket 通信
Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。
线程同步机制
用于协调多个线程的执行,以确保数据的正确性和一致性。
互斥锁:
互斥锁是最常见的线程同步机制。它允许多个线程访问共享资源,但在任何给定时刻只有一个线程可以进入临界区(被保护的代码段)。其他线程需要等待锁的释放才能进入临界区。
信号量:
信号量是一种更通用的同步工具,它可以控制同时访问共享资源的线程数量。
条件变量:
条件变量用于在线程之间进行复杂的协调。它通常与互斥锁一起使用,允许线程等待特定条件的发生,并在条件满足时被唤醒。
读写锁:
读写锁允许多个线程同时读取共享资源,但只有一个线程可以写入资源。这对于读多写少的场景非常有用,可以提高性能。
自旋锁
自旋锁是一种锁定机制,它不会使线程休眠,而是在获取锁失败时一直循环尝试,直到成功。自旋锁在短期争用情况下可能效率更高,但需要小心避免长时间的自旋。
守护进程、僵尸进程、孤儿进程
守护进程
是对立于控制终端并且周期性的执行某种任务的进程
指在后台运行的,没有控制终端与之相连的进程。它独立于控制终端,周期性地执行某种任务。Linux的大多数服务器就是用守护进程的方式实现的,如web服务器进程http等
孤儿进程
如果父进程先退出,子进程还没退出,那么子进程的父进程将变为init进程。(注:任何一个进程都必须有父进程)。
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程
父进程不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成资源的浪费!
父进程可以使用waitpid来等待子进程的退出,设置WNOHANG为非阻塞等待
如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵尸进程。
设置僵尸进程的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。
进程一旦变成僵尸进程,kill -9 (SIGKILL)也无法将其杀死,因为谁也无法杀死一个已经死去的进程。
进程/线程切换过程切换的资源有哪些
进程/线程上下文切换是操作系统进行任务切换时,保存当前任务的状态并加载下一个任务的状态的过程。在上下文切换过程中,操作系统需要保存和恢复的资源包括:
寄存器
包括通用寄存器(如PC、SP等)和特殊寄存器(如状态寄存器、控制寄存器等)。保存当前任务的寄存器状态,并加载下一个任务的寄存器状态。
程序计数器(PC)
保存当前任务执行的下一条指令的地址,以便在切换回来时继续执行。
栈指针(SP)
保存当前任务的栈指针,以便在切换回来时继续使用该任务的栈。
内存管理单元(MMU)
保存当前任务的页表、段表等内存管理信息,以便在切换回来时继续使用该任务的内存映射。
文件描述符表
保存当前任务打开的文件描述符信息,以便在切换回来时继续使用。
环境变量
保存当前任务的环境变量信息,以便在切换回来时继续使用。
其他资源
如信号处理函数、定时器、硬件中断等,需要保存当前任务的相关状态,并在切换回来时继续处理。
需要注意的是,不同操作系统和架构可能会有略微不同的上下文切换过程和需要保存的资源,上述列举的是一般情况下的常见资源。