进程和程序是操作系统中的两个核心概念,但它们并不等同。程序是静态的,是一系列指令的集合,用于描述某件事情的操作序列或算法。而进程是动态的,是程序在计算机上的一个执行实例。进程加载到内存中运行,并包含程序、输入、输出以及状态等信息。
在Linux内核中,线程和进程都使用相同的进程描述符数据结构来表示。通过clone()函数可以创建线程,它决定了哪些资源和父进程共享,哪些资源为线程独享。因此,在Linux中,线程实际上是一个特殊的进程,它们共享相同的地址空间和其他资源,但拥有独立的执行流。
程序、进程和线程是计算机操作系统中的核心概念,它们各自扮演着不同的角色。程序是静态的指令集合,用于描述完成特定任务的操作序列;进程则是程序执行时的动态实体,具有“生命力”,包含了程序的代码、活动信息和数据;线程则是进程内部的一个执行单元,是操作系统分配资源的基本单位。
进程描述符,也被称为进程控制块(PCB),是对进程所拥有的资源进行抽象的数据结构。它记录了进程的运行状态、程序计数器、CPU寄存器、调度信息、内存管理信息、统计信息以及文件相关信息等。Linux内核采用名为task_struct的结构体来表示进程描述符,该结构体包含了进程所有相关的属性和信息。
进程是操作系统调度的实体,通过对进程描述符的管理和调度,操作系统能够实现多进程并发执行和CPU虚拟化。每个进程都认为自己独立拥有一个CPU,这是通过上下文切换和进程调度等核心技术实现的。上下文切换保存了当前进程的CPU状态,以便在进程被调度回来时能够恢复执行。
线程作为进程的一个组成部分,共享进程的资源空间,但拥有独立的执行流。线程的出现提高了并发性和资源利用率,使得多个线程可以在同一进程中并发执行,共享相同的地址空间和其他资源。
在Linux内核中,进程和线程都使用相同的进程描述符来表示,没有额外的数据结构或调度算法专门为线程服务。这种设计使得线程在Linux内核中表现为一种特殊的进程,它们共享相同的资源空间,但具有独立的执行路径和状态。
进程是操作系统中的一个核心概念,用于描述程序执行时的状态和资源占用情况。在Linux内核中,进程描述符(task_struct数据结构)是操作系统用来感知和管理进程的关键数据结构。它包含了丰富的信息,可以归纳为进程属性、进程间关系、进程调度、内存管理、文件管理、信号处理和资源限制等几大类。
进程属性相关信息主要包括进程状态、唯一标识符、标志位、退出值和终止信号等。进程间的关系则描述了进程之间的父子、兄弟关系,以及进程组的组长等信息。进程调度相关信息涉及进程作为调度实体参与操作系统的调度,包括调度优先级、调度队列和调度状态等。
内存管理相关信息包括进程所使用的内存信息,如页表、内存映射等。文件管理相关信息则涉及进程打开的文件、目录以及文件系统的状态等。信号处理机制允许进程对收到的信号做出响应,如中断、终止或忽略信号。资源限制相关信息则用于对进程所使用的资源进行限制,以防止资源滥用。
task_struct数据结构将这些信息组织在一起,并通过链表等数据结构相互关联,形成一个完整的进程描述符集合。内核通过管理这些进程描述符来实现进程的创建、调度、资源分配和销毁等操作。
进程描述符的设计和实现是操作系统内核的重要部分,它对于实现进程管理、并发执行和资源共享等功能至关重要。通过对进程描述符的深入理解和分析,可以更好地理解操作系统的内部机制和工作原理。
TASK_RUNNING(可运行态或者就绪态或者正在运行态)
- 这个状态确实容易让人混淆,因为在多核系统中,可能同时有多个进程处于TASK_RUNNING状态,但它们并不都在CPU上执行。实际上,这个状态包括了那些已经准备好在CPU上执行,但可能由于调度策略或其他原因还没有真正执行的进程。
TASK_INTERRUPTIBLE(可中断睡眠态)
- 当进程需要等待某个条件成立(例如等待I/O操作完成或等待某个信号)时,它会进入这个状态。进程在这个状态下可以被信号中断,并提前唤醒。这种设计使得进程在等待时仍然可以响应外部事件。
TASK_UNINTERRUPTIBLE(不可中断态)
- 这个状态与TASK_INTERRUPTIBLE类似,但不同之处在于进程在这个状态下不会响应任何信号,直到它等待的条件满足。这通常用于那些不能被打断的等待操作,例如某些硬件操作或内核中的关键部分。ps命令中看到的D状态进程就是这种类型。
TASK_STOPPED(终止态)
- 进程在接收到某些信号(如SIGSTOP或SIGTSTP)后会进入这个状态。通常,这意味着进程已经被用户或系统管理员明确地停止了。
EXIT_ZOMBIE(僵尸态)
- 当一个进程结束时,它的task_struct并不会立即被释放。相反,它会进入僵尸状态,等待其父进程通过wait()或waitpid()系统调用来收集它的退出状态和资源使用情况。这样做是为了确保父进程能够正确地处理子进程的结束,并避免资源泄漏。
当父进程或子进程中的任何一个试图写入一个标记为写时复制的页面时,会发生一个写保护的缺页异常(page fault)。此时,内核会介入,执行以下步骤:
- 分配一个新的物理页面。
- 将原始页面的内容复制到新页面。
- 更新触发异常的进程的页表,使其指向新的物理页面,并取消写保护标记。
- 如果另一个进程(父进程或子进程)也引用了这个页面,则同样更新其页表,使其指向原始的物理页面,并标记为写时复制。
(1) 检查子进程是否允许被跟踪
在Linux中,PTRACE是一个系统调用,它允许一个进程(称为跟踪器或调试器)检查和改变另一个进程(称为被跟踪或被调试进程)的执行和行为。在创建子进程之前,可能需要检查父进程是否拥有对被跟踪进程的权限,这通常涉及到检查父进程的CAP_SYS_PTRACE能力。
(2) 调用copy_process() 函数创建一个新的子进程
copy_process()是Linux内核中创建新进程的核心函数。如果创建成功,该函数会返回新进程的task_struct数据结构。这个数据结构包含了进程的所有状态信息。
(3) 由子进程的task_struct 数据结构来获取PID
通过task_struct中的pid成员,我们可以获取进程的PID。在Linux中,PID是进程的唯一标识符。pid_nr()函数用于获取pid结构中的数值,即从当前命名空间内部看到的PID。
(4) 对于vfork() 创建的子进程
vfork()是一个创建新进程的系统调用,它与fork()的主要区别在于vfork()创建的子进程会共享父进程的地址空间,直到子进程调用exec()或exit()。这要求父进程在子进程调用这些函数之前保持阻塞状态,以免修改共享数据。使用init_completion()初始化的完成量(completion)可以用于同步,确保父进程在子进程准备好之前不会继续执行。
(5) wake_up_new_task() 函数唤醒新创建的进程
当新进程创建后,它最初处于不可运行状态。wake_up_new_task()函数负责将新进程添加到调度器的就绪队列中,使其可以接受调度并运行。
(6) 对于vfork() ,等待子进程调用exec() 或exit()
在vfork()的情况下,父进程需要等待子进程完成其初始化工作(通常是调用exec())或退出(通过exit())。这可以通过使用同步机制(如完成量)来实现,以确保父进程在子进程准备好之前不会继续执行。
(7) 返回值处理
在do_fork()函数执行后,存在两个进程:父进程和子进程。这两个进程都会从do_fork()函数的返回点继续执行。对于父进程,do_fork()返回新创建的子进程的PID;对于子进程,返回值为0。这是区分父进程和子进程的一种常用方法。
|