进程概念
获取进程pid方式(概念)
1.获取所有的pid信息
部分获取ps ajx | head -1 && ps axj | grep 文件名
ps ajx | head -1 && ps axj | grep 文件名 | grep -v grep可以过滤掉grep中携带进程部分信息的内容
因为我没有运行test.o所以未开辟进程,所以没有进程信息
ps ajx | grep 文件名
2.示例代码
每个进程都有属于自己的PID(身份码)
在Linux系统里,PID即“Process IDentifier”,是进程标识符 ,以下是详细介绍:
基本概念
PID是系统为每个进程分配的独一无二的非负整数 。每个进程从创建起就被赋予特定PID,借此在系统中被唯一标识。就像现实中每辆车都有独特车牌号,方便交通管理,PID便于操作系统对进程进行管理和区分。
作用
- 进程管理:是进程的身份标识。系统借助PID识别、跟踪、控制进程,如使用 kill 命令通过指定PID向进程发送信号,实现进程终止(如 kill -9 <PID> 可强制终止进程 ); waitpid 函数能让父进程等待特定子进程结束 。
- 资源分配与调度:操作系统依据PID管理进程资源分配,决定CPU时间、内存等资源如何分配给各进程,保障系统高效稳定运行 。
- 进程间通信:进程可利用PID识别通信对象,实现进程间通信和同步协作 。
回顾上文我们简单的了解了进程,首先介绍的是fork.
在Linux中, fork 是一个用于创建新进程的系统调用函数 ,它在Unix和类Unix操作系统中被广泛使用。以下是关于它的详细介绍:
函数原型与头文件
- 原型: pid_t fork(void); , pid_t 实质是 int ,在 <sys/types.h> 中定义 。
- 头文件:使用时需包含 <unistd.h> ,有时也会用到 <sys/types.h> 。
工作原理
调用 fork 时,系统会创建一个与调用进程(父进程)几乎完全相同的新进程(子进程)。具体过程如下:
1. 资源分配:系统为新进程分配资源,如存储数据和代码的空间。
2. 数据复制:将父进程的很多属性复制到子进程,包括数据空间、堆、栈等资源的副本,但父子进程并不共享这些存储空间 。不过,代码段是共享的,即父子进程从 fork 函数调用之后的下一条指令开始执行。
返回值特点
fork 调用一次却返回两次,在不同进程中有不同返回值:
- 在父进程中:返回新创建子进程的进程ID(PID),是一个大于0的整数 。通过这个返回值,父进程能识别并后续操作子进程。
- 在子进程中:返回值为0 。子进程可据此判断自身身份。
- 出错时:返回 -1 ,可能原因有当前进程数达到系统上限( errno 设为 EAGAIN ) 或系统内存不足( errno 设为 ENOMEM )。
在上述代码中,执行 fork 后产生父子进程。子进程中 fpid 为0,打印子进程相关信息;父进程中 fpid 是子进程PID,打印父进程及子进程PID信息 。
应用场景
- 多进程并发服务器:有客户端连接时,父进程用 fork 创建子进程处理请求,自身继续监听新连接,提升并发处理能力 。
- 任务并行处理:将不同任务分发给子进程并行执行,加快任务完成速度,比如批量文件处理等场景 。
提出以下问题
1. 为什么 fork 要给子进程返回0,给父进程返回子进程 pid
- 子进程返回0:子进程通过返回0能方便地知道自己是子进程,因为0是一个特殊值,与任何有效的进程ID都不同。这样子进程可以在后续代码中根据这个返回值来执行特定于子进程的逻辑。
- 父进程返回子进程 pid :父进程得到子进程的 pid 后,就可以通过这个 pid 来对特定的子进程进行管理和控制,比如等待子进程结束、向子进程发送信号等。同时,父进程可以同时创建多个子进程,通过不同的 pid 来区分和处理各个子进程。
2. 一个函数是如何做到返回两次的
fork 函数会创建一个新的进程,即子进程。子进程是父进程的一个副本,它从 fork 函数调用处开始执行,就好像是父进程的一个“分身”。在 fork 函数执行时,内核会为子进程分配资源,并复制父进程的上下文,包括程序计数器、寄存器等。因此,看起来 fork 函数在父进程和子进程中都被执行了一次,也就有了两次返回。一次是在父进程中继续执行,返回子进程的 pid ;另一次是在子进程中执行,返回0。二者返回存在顺序问题,我们无法表象过去,个人认为父进程优先于子进程返回,因此先输出父进程内容,后输出子进程内容。
3. 一个变量怎么会有不同的内容?如何理解?
在 fork 之后,父进程和子进程拥有各自独立的地址空间。虽然它们在 fork 之前的变量值是相同的,但 fork 之后,对一个进程中变量的修改不会影响到另一个进程中的同名变量。这是因为它们的地址空间是相互独立的,每个进程都有自己的一份变量副本。例如,父进程中的一个全局变量 x ,在 fork 之后,父进程和子进程都有自己的 x ,父进程修改自己的 x 不会影响到子进程的 x ,反之亦然。
fork 后父子进程代码共享、数据单独开辟,主要有以下原因:
代码共享(写时拷贝)
- 提高内存利用效率:程序代码通常是只读的,父子进程执行相同的程序代码,若各自复制一份到内存,会浪费大量空间。共享代码段能让多个进程共用同一份代码,减少内存占用。
- 保证代码一致性:共享代码可确保父子进程执行的代码完全一致,避免因代码复制产生不一致或错误。
数据单独开辟
- 进程独立性要求:每个进程需有独立数据空间,保证其数据操作不影响其他进程。父子进程后续可能执行不同任务,对数据有不同修改需求,若数据不独立,一个进程对数据的修改会影响另一个进程,导致程序逻辑混乱。
- 数据安全与稳定:单独开辟数据空间为进程提供了安全稳定的运行环境。一个进程的数据损坏或异常不会波及其他进程,增强了系统的稳定性和可靠性。
4. fork 函数究竟是什么,干什么
fork 函数是UNIX和类UNIX系统中的一个系统调用,用于创建一个新的进程。它的主要作用是将当前进程(父进程)复制一份,生成一个新的进程(子进程)。子进程在许多方面与父进程相似,包括程序代码、数据段、堆、栈等,但它们是两个独立的进程,有各自的进程控制块(PCB)和独立的地址空间。这样,父进程和子进程可以并发执行,各自执行不同的任务,从而实现多任务处理。例如,一个服务器程序可以通过 fork 函数创建多个子进程来同时处理多个客户端的请求。
进程状态
操作系统学科进程状态(运行,阻塞,挂起)
进程状态与内存管理
在Linux系统中,进程状态丰富多样且与内存管理紧密相连。当进程处于阻塞状态时,往往是在等待特定事件,像从键盘读取数据。此时,进程会被挂起,其关键信息存储于 struct task_struct 结构体中。若操作系统内存资源严重匮乏,部分进程将被换出至交换分区(swap),以此节省内存,保障系统正常运转。待条件满足,比如所需数据准备就绪,进程又会被换入内存继续执行。
struct task_struct 是Linux内核用于描述进程的重要结构体,涵盖进程状态、优先级等众多属性。而 struct dev 则用于描述设备,包含设备类型、状态,以及指向相关进程的指针等信息。部分设备结构体还设有等待队列,用于管理那些等待该设备资源的进程。
进程调度与运行
系统中存在一个由调度器管理的运行队列( struct runqueue ) ,该队列有指向队头和队尾进程的指针。调度器的职责是从运行队列里挑选合适的进程,使其能在CPU上运行。进程进入运行队列,意味着它已准备就绪,随时可被调度。
进程在CPU上并非一直运行至结束才释放资源,而是引入了时间片的概念(如图中示例的10ms )。当进程的时间片耗尽,即便尚未执行完毕,也会被调度器从CPU上移除,排至运行队列末尾,等待下一轮调度。这种频繁的进程上下操作,即进程切换,让所有进程的代码在一个时间段内都能得以执行,达成并发执行的效果。比如,即便进程遭遇 while(1); 这样的死循环代码,时间片机制也能确保其他进程有机会获得CPU调度。
补充完善
进程状态转换十分复杂,除阻塞和运行状态外,还有就绪状态、睡眠状态等。就绪状态的进程已万事俱备,只待CPU资源,一旦CPU空闲且被调度器选中,便会转入运行状态。睡眠状态又细分为可中断睡眠与不可中断睡眠,前者可被信号唤醒,后者一般在等待特定I/O操作完成后才会苏醒。
调度器选择进程的依据是不同的调度算法。常见的有先来先服务(FCFS)、短作业优先(SJF)、时间片轮转(RR)、优先级调度等。Linux系统采用的完全公平调度器(CFS)更为复杂高效,它通过统计和比较进程的虚拟运行时间,公平分配CPU时间,防止某些进程长时间得不到调度。
内存管理方面,当内存资源不足时,操作系统在选择换出进程至交换分区时,会综合考量进程活跃度、内存使用量等因素。通常,不活跃进程更易被换出。进程换入内存时,需兼顾内存空间分配与进程需求,保障系统高效运行。此外,操作系统还借助页缓存等缓存机制,降低进程换入换出的性能开销,提升数据访问速度。
linux的维护方法
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
以下是对进程状态的解释:
- R (running):运行中,进程正在执行或等待 CPU 资源。
- S (sleeping):睡眠状态(可中断),等待事件完成(如 I/O 操作),可被信号唤醒。
- D (disk sleep):磁盘睡眠(不可中断),等待磁盘 I/O 完成,无法被信号唤醒。
- T (stopped):停止状态,进程被暂停(如收到 SIGSTOP 信号)。
- t (tracing stop):追踪停止,因调试被追踪器暂停。
- X (dead):死亡状态,进程已终止,即将被系统移除(内核内部状态,一般不可见)。
- Z (zombie):僵尸状态,进程已结束但父进程未读取其退出状态,资源未完全释放。
说明:状态值为对应下标,可通过位运算判断组合状态(如 S+D 对应 1|2=3 )。
Z(zombie)-僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)
没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
#include <stdio.h>
#include <stdlib.h>
int main()
{pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id > 0){ //parentprintf("parent[%d] is sleeping...\n", getpid());sleep(30);}else{printf("child[%d] is begin Z...\n", getpid());sleep(5);exit(EXIT_SUCCESS);}return 0;
}
僵尸进程危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话
说,Z状态一直不退出,PCB一直都要维护?是的!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
内存泄漏?是的!
如何避免?后面讲
进程状态总结
至此,值得关注的进程状态全部讲解完成,下面来认识另一种进程
孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为“孤儿进程”
孤儿进程被1号init进程领养,当然要有init进程回收喽。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){//childprintf("I am child, pid : %d\n", getpid());sleep(10);}else{//parentprintf("I am parent, pid: %d\n", getpid());sleep(3);exit(0);}return 0;
}