目录
进程的定义
进程可以被视为一个执行中的程序实例,它包含了程序代码、数据集以及一个描述其状态和控制信息的进程控制块(Process Control Block, PCB)。操作系统通过PCB来跟踪和控制进程的执行状态,包括进程的ID、内存映射、打开的文件、状态信息等。
进程的特征
- 动态性:进程是程序的一次执行过程,有创建、执行和消亡的生命周期。
- 并发性:多个进程可以并发执行,共享处理器和其他系统资源。
- 独立性:每个进程都有自己独立的内存空间和系统资源,相互之间不受影响。
- 异步性:进程的执行是异步的,即进程的执行速度不可预知,受到多种因素的影响。
进程的状态
进程在其生命周期中会处于不同的状态,主要包括:
- 就绪状态:进程已经准备好执行,等待CPU资源。
- 执行状态:进程正在使用CPU执行指令。
- 阻塞状态:进程因为等待某种事件(如I/O操作完成)而暂时不能执行。
进程与程序的区别
- 程序是一组指令的集合,是静态的;而进程是程序在某个数据集上的一次执行过程,是动态的。
- 程序是永久的,存储在磁盘上;进程是暂时的,存在于内存中,随程序的启动而创建,随程序的结束而消亡。
进程的控制和管理
操作系统负责管理和控制进程,主要包括:
- 进程创建:操作系统为新进程分配资源,初始化PCB。
- 进程调度:决定下一个执行的进程,将CPU分配给进程。
- 进程同步:协调多个进程对共享资源的访问,防止冲突。
- 进程通信:允许进程间交换信息,支持进程协同工作。
- 进程终止:回收进程占用的资源,结束进程的生命周期。
进程的特点
1. 虚拟内存空间的分配
- 虚拟地址空间:在典型的32位系统中,进程拥有一个4GB的虚拟地址空间,这个空间被分为两部分:
- 用户空间(0-3GB):这部分空间专属于每个进程,用于存放代码、数据、堆和栈等,是进程的私有区域,其他进程无法直接访问。
- 内核空间(3-4GB):这部分空间是所有进程共享的,主要存放操作系统内核代码和数据结构。当进程需要与内核交互时,如进行系统调用,会进入内核空间。
2. 时间片轮转调度
- 时间片调度:操作系统采用时间片轮转(Round Robin, RR)调度策略来管理多个进程。每个进程在获得CPU执行权时,会被分配一个固定的时间片(通常几毫秒到几十毫秒不等),在此期间,进程独占CPU资源执行。
- 多任务处理:通过快速地在多个进程之间切换执行(时间片轮转),操作系统可以给用户创造出多任务并行执行的错觉,即使在单核CPU上也能实现高效的多任务处理。
图解:
进程段
数据段(Data Segment)
-
存储内容:数据段主要用于存储进程中的全局变量、静态变量以及动态分配的内存。全局变量是指在程序的多个函数间共享的数据,而静态变量则是在函数外部声明但在函数内部使用的变量,它们在程序的整个生命周期中都保持有效。
-
动态数据分配:使用如
malloc
、calloc
、realloc
和free
等函数动态分配的内存也位于数据段中,这部分内存通常被称为“堆”(Heap),它提供了灵活的内存管理能力,允许程序在运行时根据需要动态地分配和释放内存空间。
正文段(Text Segment)
- 存储内容:正文段,也称作代码段或文本段,主要用于存储程序的可执行代码。这些代码是预先编译好的机器指令,由操作系统加载到内存中供 CPU 执行。正文段通常具有只读属性,以防止程序意外修改自己的代码,从而提高程序的稳定性和安全性。
堆栈段(Stack Segment)
- 存储内容:堆栈段是用于存储函数调用过程中产生的临时数据的区域,主要包括:
- 函数的局部变量:这些变量在函数调用时创建,在函数退出时销毁。
- 函数调用的参数:当一个函数被调用时,其参数会按照一定的顺序压入堆栈。
- 返回地址:记录调用者的位置,以便函数执行完毕后返回到正确的指令处继续执行。
- 工作原理:堆栈段遵循“先进后出”(LIFO, Last In First Out)的原则,通过压栈(PUSH)和弹栈(POP)操作来管理数据的进出,这种机制简化了函数调用的管理,使得函数调用和返回的控制流更加清晰和高效。
进程分类
交互进程
- 特性与用途:交互进程通常由 shell 启动,直接与用户交互,响应用户输入并展示输出结果。这类进程既可以运行在前台,占据用户的终端界面,也可以运行在后台,不干扰用户的其他操作。典型的交互式进程包括 shell 命令、文本编辑器(如 vi 或 nano)、图形界面应用程序等。
批处理进程
- 特性与用途:批处理进程不直接与用户交互,它们通常被提交到一个队列中,按顺序执行。这类进程主要用于处理大量数据或执行长时间运行的任务,如批量数据处理、备份作业、系统维护任务等。由于不需要用户实时干预,批处理进程可以高效利用系统资源,避免因用户交互而引起的中断和延迟。
守护进程
- 特性与用途:守护进程是长期运行在后台的服务进程,它们在系统启动时被激活,持续运行直到系统关闭。守护进程不依附于任何终端,主要负责提供系统级的服务,如网络服务(HTTP、FTP)、系
进程状态
-
新建状态(New): 当一个进程被创建时,它首先进入新建状态。此时,操作系统为其分配必要的资源,如内存、文件描述符等,并初始化进程控制块(PCB)。进程尚未被调度执行。
-
就绪状态(Ready): 进程已经准备好执行,但由于当前CPU正忙于其他进程,它需要等待调度器的调度。在就绪状态下的进程位于就绪队列中,等待CPU时间片。一旦被调度,进程将从就绪状态转变为执行状态。
-
执行状态(Running): 进程正在使用CPU执行指令。在多任务操作系统中,执行状态的进程可能很快就会因为时间片到期、更高优先级的进程就绪或进程自身请求I/O操作等原因而转变为其他状态。
-
阻塞状态(Blocked): 也称为等待状态或睡眠状态。当进程在执行过程中遇到I/O操作或其他需要等待的事件时,它会主动或被动地进入阻塞状态。此时,进程放弃CPU使用权,等待相应的事件发生,如I/O操作完成、信号量变为可用等。阻塞状态下的进程不参与CPU调度,直到阻塞原因解除。
-
僵尸状态(Zombie): 当一个子进程结束执行但其父进程尚未对其进行善后处理(如调用
wait()
或waitpid()
函数)时,子进程会进入僵尸状态。僵尸进程仍然占用一部分系统资源,如进程表中的一个条目,但不再占用CPU时间,也不会执行任何指令。 -
终止状态(Terminated): 进程执行完毕或因异常而终止后,会进入终止状态。在终止状态下,进程实际上已经停止运行,但其状态信息仍保留在系统中,直到父进程清理这些信息(通过调用
wait()
或waitpid()
)。清理后,进程完全消失,释放所有资源。
进程状态切换
进程创建后,进程进入就绪态,当CPU调度到此进程时进入运行态,当时间片用完时,此进程会进入就绪态,如果此进程正在执行一些IO操作(阻塞操作)会进入阻塞态,完成IO操作(阻塞结束)后又可进入就绪态,等待CPU的调度,当进程运行结束即进入结束态。
C语言中进程函数
创建进程fork
pid_t fork(void);
功能:创建子进程
返回值:
成功:在父进程中:返回子进程的进程号 >0
在子进程中:返回值为0
失败:-1并设置errno
-
资源与状态的继承与分离:
- 子进程几乎完全复制了父进程的执行上下文,包括代码段、数据段、栈段、文件描述符等,但它们拥有独立的进程ID(PID)和父进程ID(PPID)。这意味着子进程继承了父进程的运行环境,但二者在系统层面是完全独立的实体。
-
独立的地址空间与数据独立性:
- 尽管子进程继承了父进程的内存映像,但它拥有自己独立的地址空间。因此,对全局变量、静态变量的修改在各自进程中是独立的,互不影响。这种隔离性确保了进程间的数据安全。
-
孤儿与僵尸进程的处理:
- 若父进程在子进程之前终止,子进程将被系统中的初始化进程(init)收养,成为孤儿进程。孤儿进程会继续执行,直到自然结束或被其他进程显式终止。
- 当子进程结束,但父进程未能及时回收其资源时,子进程会变成僵尸进程。僵尸进程虽已结束,但仍占用系统资源,直到父进程通过
wait()
或waitpid()
系统调用来回收为止。应避免产生僵尸进程,以维持系统资源的高效利用。
-
文件描述符的共享与独立操作:
- 子进程继承了父进程打开的文件描述符,这意味着同一文件在父进程和子进程中可通过相同的描述符访问。尽管如此,文件操作(如读写位置)在各自的进程中是独立的,不会互相干扰。
fork() 调用总结
-
执行流程与返回值:
fork()
调用在父进程中返回子进程的PID,在子进程中返回0。若返回负数,则表示创建子进程失败。fork()
调用仅执行一次,但它的效果是使得代码在父进程和子进程中分别执行一次,形成两个独立的执行流。
-
资源复制与独立执行:
fork()
创建子进程时,子进程获得了父进程的资源副本,包括代码段、数据段和栈段,但拥有独立的地址空间和资源管理。- 父子进程在
fork()
后的代码段是独立执行的,即使它们执行相同的指令序列,也不会相互影响。
注意事项
在使用 fork()
创建子进程时,应注意以下几点:
- 资源管理:确保父进程和子进程妥善管理共享资源,如文件描述符和信号量,以避免资源泄露或竞争条件。
- 进程清理:父进程应负责回收子进程的资源,避免产生僵尸进程,保持系统资源的健康状态。
- 信号处理:考虑进程间的信号传递机制,以实现进程间的协调和控制,特别是在异常处理和进程终止时。
回收进程wait waitpid
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);// 函数原型
pid_t wait(int *status);
功能:回收子进程资源(阻塞)
参数:status:子进程退出状态,不接受子进程状态设为NULL
返回值:成功:回收的子进程的进程号
失败:-1
参数说明
-
pid
:用于指定要等待的子进程的 PID(进程ID)或一组进程的选择标准:>0
:等待特定 PID 的子进程。-1
:等待任意一个子进程,这是最常见的用法。=0
:等待与调用进程具有相同进程组 ID 的任意子进程。<-1
:等待与pid
绝对值对应的进程组 ID 相同的任意子进程。
-
status
:指向一个整型变量的指针,用于存储子进程的退出状态。如果子进程正常结束,status
中包含的值可以被WEXITSTATUS()
宏解析得到子进程的退出码;如果是信号导致子进程终止,status
中的值可以被WTERMSIG()
宏解析得到终止信号。 -
options
:指定waitpid()
的行为模式:0
:默认阻塞模式,父进程将暂停执行,直到子进程结束。WNOHANG
:非阻塞模式,立即返回,无论是否有子进程结束。如果无子进程结束,返回0
;否则返回结束子进程的 PID。
返回值
- 正常情况:返回结束子进程的 PID。
- 非阻塞模式且没有子进程结束:返回
0
。 - 出错:返回
-1
,并设置errno
变量以指示错误原因。
退出进程exit _exit
void exit(int status);
功能:结束进程,刷新缓存
参数:退出的状态
不返回。
void _exit(int status);
功能:结束进程,不刷新缓存
参数:status是一个整型的参数,可以利用这个参数传递进程结束时的状态。
通常0表示正常结束;
其他的数值表示出现了错误,进程非正常结束
获取进程号 getpid getppid
pid_t getpid(void);
功能:获取当前进程的进程号
pid_t getppid(void);
功能:获取当前进程的父进程号
return: 关键字,在子函数中返回到函数调用的位置,并不结束进程(函数的退出)
exit :函数,不管在子函数还是主函数都会结束进程(进程的退出)
总结:
在计算中进程是无处不在的,更好的了解进程也可以让我们更好的了解计算机以及编程的原理,文章至此结束,希望可以帮到大家。万分感谢看到这里。