操作系统之进程和线程(二者的区别,进程的状态切换、创建、终止、上下文切换)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/u014454538/article/details/99330919

1. 进程的概述

① 进程和线程
  • 进程(Process)是资源分配的基本单位,线程(Thread)是CPU调度的基本单位。
  • 线程将进程的资源分和CPU调度分离开来。 以前进程既是资源分配又是CPU调度的基本单位,后来为了更好的利用高性能的CPU,将资源分配和CPU调度分开。因此,出现了线程。
  • 进程和线程的联系: 一个线程只能属于一个进程,一个进程可以拥有多个线程。线程之间共享进程资源。
    在这里插入图片描述
  • 进程和线程的实例: 打开一个QQ,向朋友A发文字消息是一个线程,向朋友B发语音是一个线程,查看QQ空间是一个线程。QQ软件的运行是一个进程,软件中支持不同的操作,需要由线程去完成这些不同的任务。
② 进程和线程的区别
  • 广义上的区别:
  1. 资源: 进程是资源分配的基本单位,线程不拥有资源,但可以共享进程资源。
  2. 调度: 线程是CPU调度的基本单位,同一进程中的线程切换,不会引起进程切换;不同进程中的线程切换,会引起进程切换。
  3. 系统开销: 进程的创建和销毁时,系统都要单独为它分配和回收资源,开销远大于线程的创建和销毁;进程的上下文切换需要保存更多的信息,线程(同一进程中)的上下文切换系统开销更小。
  4. 通信方式: 进程拥有各自独立的地址空间,进程间的通信需要依靠IPC;线程共享进程资源,线程间可以通过访问共享数据进行通信
  • Linux系统中进程和线程的区别:
  1. 在Linux系统中,内核调度的单元是struct task_struct,每个进程对应一个task_struct
  2. 2.6以前的内核中没有线程的概念,内核将线程视为轻量级进程(LWP),并为每一个线程分配一个task_struct
  3. 2.6以后的内核中出现了线程组的概念,同一个进程中的线程放入一个线程组中;内核仍然视线程为轻量级进程,每个task_struct对应一个进程或者线程组中的一个线程
  4. 如果线程完全在内核态中实现(内核线程,KLT),内核调度的单元是线程。此时,进程与线程的区别非常微妙。
  5. 如果线程完全在用户态实现(用户线程,ULT),内核调度的单元是进程,内核对用户线程一无所知。内核只负责分配CPU给进程,进程得到CPU会后再分配给内部的线程
    在这里插入图片描述
③ 进程的组成
  • 进程由程序代码数据进程控制块Process Control Block, PCB)三个部分组成,即进程映像Process Image)。
  • 关于PCB :
  1. PCB描述进程的基本信息运行状态,所谓的创建撤销进程,都是指对 PCB 的操作。
  2. PCB是一个数据结构,它常驻内存,其中的进程ID(PID)唯一标识一个进程。
  3. 标识符:自身ID(PID)、父进程ID(PPID)、用户ID(UID)
  4. 处理机状态:主要由处理机的各种寄存器中的内容组成,包括通用寄存器程序计数器(PC),存放下一条要访问的指令地址;程序状态字(PSW),包含条件码、执行方式、中断屏蔽标志等状态信息;用户栈指针,存放过程和系统调用的参数及调用地址。
  5. 进程调度信息 :包括进程状态,指明进程的当前状态;进程优先级进程调度所需的其它信息,如进程已等待CPU的时间总和、进程已执行的时间总和等;事件,由执行状态转变为阻塞状态所等待发生的事件,即阻塞原因。
  6. 进程控制信息:包括程序和数据的地址,是指进程的程序和数据所在的内存或外存地址;进程同步和通信机制,指实现进程同步和进程通信时必需的机制,如消息队列指针、信号量等;资源清单,进程所需的全部资源及已经分配到该进程的资源的清单;链接指针
  • PCB的组织方式: 链接和索引
  1. 链接:运行态、就绪态、阻塞态分别维护一个链表,每种状态的PCB通过链表连接。其中就绪态的链表只有一个PCB,因为同一时刻只有一个进程处于就绪态。
    在这里插入图片描述
  2. 索引: 运行态、就绪态、阻塞分别维护一个PCB表,该表中的每个entry指向一个PCB。
    **加粗样式**
  • 每个进程都有自己的PID,进程依靠进程树进行组织。其中根进程PID = 1,父进程的撤销会撤销全部的子进程。
    在这里插入图片描述

2. 进程的生命周期

① 进程的状态切换
  • 关于几种状态:
  1. 就绪态: 刚创建的进程完成PCB的初始化后,被加到就绪队列中,等待CPU的调度。
  2. 运行态: 处于就绪态的进程获得CPU时间后,变为运行态。运行态的进程使用完CPU时间后,变为就绪带态。只有就绪态和运行态可以相互转换
  3. 阻塞态: 进程在运行的过程中由于缺少必须的资源,变为阻塞态。注意: 资源不包括CPU时间,一般指I/O操作等。
  4. 挂起态: 五状态模型中,缺少对I/O操作的描述,于是增加交换(Swapping) 这一概念。处于阻塞态的进程由于长时间未等待到资源,通过换出将其从内存转到外存变为挂起态,这样可以减少内存占用;当资源可用时,处于挂起态的进程通过换入外存转到内存变为就绪态
    在这里插入图片描述
② 进程的创建

在这里插入图片描述

  • Linux系统中的进程创建:
  1. 父进程调用fork()vfork()clone()中的任一方法创建新进程,这些方法对应的系统调用入口分别为sys_fork()sys_vfork()sys_clone()。这些系统调用内部都会调用do_fork()函数完成进程创建,只是携带的参数不同。
  2. do_fork()函数会调用copy_process()函数,执行进程创建的实际工作
  3. copy_process()函数调用dup_task_struct()函数复制当前的task_struct
  4. copy_process()函数调用shed_fork()函数初始化进程数据结构,将进程状态设位置为 TASK_RUNNING(统计结果是: 1,只有该状态的进程才可能在CPU上运行)。
  5. copy_process()函数复制父进程的所有信息
  6. copy_process()函数调用copy_thread()函数初始化子进程的内核栈copy_thread()函数会将子进程的返回值设为0:childregs->ax = 0返回地址设为ret_from_fork: p->thread.ip = (unsigned long) ret_from_fork;,因此子进程是从ret_from_fork开始执行的。
  7. copy_process()函数为子进程分配并设置PID。至此,子进程准备就绪,等待被调度。
  • 关于进程创建的一些说明:
  1. 新创建的子进程和父进程拥有相同的代码段,代码段的内容是从fork()函数之后开始的。即从ret_from_fork开始的。
  2. 若创建后的子进程想要执行不同的程序,可以调用exec()函数族覆盖进程映像,但是子进程的PID不会发生改变。
  3. fork()函数调用失败返回-1,调用成功在父进程中返回子进程的PID,在子进程中返回0
  4. 子进程和父进程拥有相同的代码段,但是由于fork()函数的返回值不同,导致子进程和父进程会打印不同的信息。
  5. 子进程可以通过getpid()获取自己的PID,通过getppid()获取父进程的PID父进程只能通过getpid()获取自身的PID,要想获取子进程的PID,必须使用变量保存fork()函数的返回值。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    pid_t pid;
    int count=0;
    pid = fork();
    if(pid < 0)
    {
        perror("fork failed");
        exit(1);
    }
    if(pid == 0)
    {
        printf("This is the child process. My PID is: %d. My PPID is: %d.\n", getpid(), getppid());
        count++;
    }
    else
    {
        printf("This is the parent process. My PID is %d, child process PID is %d.\n", getpid(), pid);
        count++;
    }
    printf("统计结果是: %d\n", count);
    return 0;
}
This is the child process. My PID is: 12778. My PPID is: 12777
统计结果是: 1
This is the parent process. My PID is 12777, child process PID is 12778.
统计结果是: 1
  • fork()函数:
  1. 传统的fork()函数,子进程复制父进程的虚拟空间,共享父进程的代码段,数据段、栈、堆都会分配新的物理内存。除了PID和PCB中的某些参数不同之外,子进程是父进程的精确复制
  2. Linux下的fork()函数,采用写时复制Copy on Write,CoW):内核一开始只为子进程复制父进程的虚拟空间并不分配物理内存,而是共享父进程的物理内存。当子进程或者父进程想要修改物理内存时,再为子进程分配单独的物理内存。
  3. fork() 函数执行后,子进程和父进程的运行是无关的,二者执行的先后顺序不固定
  • vfork()函数:
  1. fork()函数更加激进,子进程直接共享父进程的虚拟空间
  2. 如果子进程修改了父进程地址空间,父进程被唤醒时会发现发现自己的数据被改了,完整性丢失,所以这是不安全的
  3. vfork()函数保证子进程先运行,只有当子进程退出(调用_exit()函数)或者执行其他程序(调用exec()函数),被阻塞的父进程才可能被调度运行。
  4. 如果在调用_exit()函数和exec()函数之前,子进程依赖父进程的进一步动作,则会导致死锁
  5. vfork()函数一般紧接着调用exec()函数,这时会为子进程分配新的物理内存,省掉了fork()函数中笨重的数据复制过程。 可以说,vfork()函数就是为了exec()函数而生
  • clone()函数: 留给轻量级进程,提供选项,自己选择复制哪些信息
③ 进程的终止
  • 进程终止的原因:
  1. 正常结束: exit、halt、logoff。
  2. 异常结束: 无可用内存、越界、保护错误、算术错误、I/O失败、无效指令、特权指令等
  3. 外界干预: kill进程、父进程终止(父进程终止,其子进程可能被系统终止)、父进程请求(父进程可以请求终止子进程)。
  • 进程终止的过程:
  1. 检索PCB,检查进程状态
  2. 将进程从运行态转换成终止态
  3. 检查是否有子进程需要终止
  4. 将获取到的资源归还给父进程或系统
  5. 将该进程的PCB从PCB队列中移出
④ 进程的上下文切换
  • 上下文切换: 是指CPU从一个进程(或线程)切换到另一个进程(或线程),涉及到控制权的转移。
  1. 保存当前进程的上下文
  2. 选择某个进程,恢复其上一次换出时保存的上下文
  3. 当前进程将控制权转移给新恢复的进程
  • task_struct是Linux中的PCB,进程上下切换时需要保存task_struct中的内容。包含以下内容:
  1. 标识符: 唯一标识一个进程
  2. 状态: 记录进程状态,如阻塞、就绪、运行等状态
  3. 优先级: 记录进程的优先级,可以根据优先级对进程执行调度
  4. 程序计数器PC: 指向进程中下一条将要执行的指令
  5. 内存指针: 程序代码和进程相关诗句的指针
  6. 上下文数据: 进程运行时,CPU中寄存器的内容
  7. I/O状态信息: 显示的I/O请求,分配给进程的I/O设备、被进程使用的文件列表等
  8. 记账信息: 处理器的时间总和、记账号等
  • 线程的切换: 线程共享进程的资源,进行线程切换时,只需要保存线程的私有数据:栈、程序计数器、寄存器
  • 进程切换的开销比线程切换的开销大:
  1. 显式原因: 进程的上下文切换需要保存更多的信息,比线程的上下文切换开销更大。
  2. 隐式原因: 进程切换时使用不同资源的task_struct之间的调度,而线程切换是使用相同资源的task_struct之间的调度。进程切换使得原有的缓存不适用,会触发缺页中断;而线程切换时,由于使用相同资源,缓存的命中率更高很多
  3. Linux中使用TLBTranslation Lookaside Buffer,转换检测缓冲)来管理虚拟内存到物理内存的映射关系。虚拟内存更新后,TLB也需要刷新,内存的访问会随之变慢。

猜你喜欢

转载自blog.csdn.net/u014454538/article/details/99330919