Linux进程概念
程序与进程定义
程序是一个普通文件,或者可以俗称程序员写的代码,它是为了完成特定任务而准备好的指令序列和数据的集合,经过编译后,可以直接执行。比如Windows下使用C语言编写的程序编译链接后可以生成一个.exe的可执行程序,生成的这个可执行程序就是一个二进制程序。计算机的CPU可以直接识别二进制,不断的从内存中取指令和数据,按照程序顺序,执行代码。
进程(process)是一个已经开始执行但还没有终止的程序实例。程序被加载到计算机内存中运行,即运行中的程序。操作系统中所有进程实体共享着计算机系统的CPU、内存、外设等资源,但它们的内存是独立的。
进程由以下几部分组成:
- 已分配内存的地址空间
- 安全属性,包括所有权凭据和特权
- 程序代码的一个活多个执行线程
- 进程状态
线程:程序执行流的最小单位,操作系统进行调度的基本单位,现代计算机为提高效率而设计。一个进程至少有一个线程,进程产生的线程是共享进程的内存资源的。
进程是程序的动态执行,一旦运行就会有一个进程ID,程序一旦运行结束就会将所占硬件资源释放掉。简单来说进程 = 内核数据结构+代码和数据。
进程类似于人类:它们被产生,有或多或少有效的生命,可以产生一个或多个子进程,最终都要死亡。一个微小的差异就是进程之间没有性别差异——每个进程都只有一个父亲。
那么操作系统该如何管理进程呢?答案就是先描述,再组织
,就是用结构体把各个进程描述出来,比如进程的状态,优先级等。然后再用数据结构把这些结构体联系起来。为了管理进程,内核必须对每个进程所做的事情进行清除的描述。例如,内核必须知道进程的优先级,它是正在CPU上运行还是因某些事情而被阻塞,给她分配了什么样的地址空间,允许它访问哪个文件等等。
课本上称之为PCB(process control block
),Linux操作系统下的PCB是: task_struct
。在Linux中描述进程的结构体叫做task_struct
。task_struct
是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_struct分类
- 标示符:描述本进程的唯一标示符,用来区别其它进程。
- 状态:任务状态,退出代码,退出信号等。
- 优先级:相对于其它进程的优先级。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其它进程共享内存块的指针。
- 上下文数据:进程执行时处理器的寄存器中的数据。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
查看进程
我们可以通过指令ps ajx或ps aux
来查看当前的所有进程
上图中的PID是进程的唯一标识符
我们可以自己编写一个代码来查看当前代码的进程,现在我有一个test.c
的文件,可以用死循环的方式让其一直执行。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{while(1){sleep(1);}return 0;
}
接着我们通过grep
指令配合ps ajx
来查看当前代码的进程,输入ps ajx | grep test.exe
。必须要再开一个窗口才能输入这个指令。
此时我们看到了test.exe
的进程了,它的PID为14436。我们要的是自己程序的进程,为什么下面还有一个进程呢?其实第二个进程是grep指令,因为我们通过grep指令过滤出test.exe
程序,所以查找进程的时候,也可以查找到grep自己。
我们也可以通过系统调用来获取进程标识符,其中父进程的id叫做PPID,子进程的id叫做PID。我们先通过man
手册来看一下getpid()
接口的含义。
getpid()
返回的是调用进程的进程 ID,而getppid()
返回的是调用进程的父进程的进程 ID。它们的使用需要包含2个头文件,返回类型是pid_t
,本质上是一个int类型并且不需要传参。
接着我们把test.c文件的代码进行更改。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{while(1){printf("pid: %d\n", getpid());printf("ppid: %d\n", getppid());}return 0;
}
再通过ps ajx
指令查找指定的PID就可以直接找到这个进程了。
可以看到,我们确实可以查到PID
为14988
的进程,并且COMMAND
属性为./test.exe
,意思就是我们通过指令./test.exe
执行了该进程。同理也可以找到父进程的PID。
这里要提出一个重要的概念:一切在命令行调用的进程,都是bash
的子进程
/proc
进程的信息可以通过 /proc 系统文件夹查看,这是一个循文件系统。/proc
的主要作用是
- 提供系统运行时的信息
- 可以获取有关进程、内存使用、CPU 信息等各种系统状态的实时数据
- 例如,通过查看
/proc/cpuinfo
可以了解 CPU 的详细信息,包括型号、核心数等。
- 监控进程状态
- 每个正在运行的进程在
/proc
下都有对应的目录,目录名即为进程的 PID。 - 可以查看进程的内存使用情况(如
/proc/<PID>/status
)、打开的文件(/proc/<PID>/fd
)等。
- 每个正在运行的进程在
- 调整系统参数
我们先来看一下/proc目录里面有什么
其中蓝色的文件表示目录。可以看到/proc内部大部分都是以数字命名的目录。而这个数字代表当前进程对应的PID,现在我们来查看一下刚才进程为14510的目录里面有什么。
其中有很多文件,这里解释其中2个比较重要的
- cwd:代表的是该进程当前的工作目录
例如:上图bash进程的工作目录是/home/HJW/linux-learning/process
- exe:代表该进程对应的可执行程序的路径
fork
fork
函数的主要作用是创建一个新的进程。具体来说,fork
函数会创建一个与当前进程几乎完全相同的子进程。我们先来学习fork函数的重要特点和作用
- 复制进程状态
- 子进程会继承父进程的许多属性,如打开的文件描述符、环境变量、当前工作目录等。
- 返回值不同
- 在父进程中,
fork
函数返回子进程的进程 ID(PID)。 - 在子进程中,
fork
函数返回 0 。
- 在父进程中,
- 实现并发编程
- 通过创建子进程,可以让父进程和子进程同时执行不同的任务,实现并发操作。
通过man
手册来来进行了解。
- 通过创建子进程,可以让父进程和子进程同时执行不同的任务,实现并发操作。
接着我们通过一个示例来演示一下。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{pid_t ret = fork();printf("hello proc!pid = %d! ppid = %d! ret = %d\n",getpid(),getppid(),ret);return 0;
}
第一行是父进程,第二行是子进程。代码中只有一个输出语句,但是结果确有2个,这是为什么呢?这是因为 fork
函数创建了一个新的进程,使得父进程和子进程都会执行后续的代码,但 fork
函数的返回值和 getpid
函数返回的当前进程 PID 在父进程和子进程中是不同的。
第一行语句hello /proc
的PID和PPID是由test.exe
创建的。而第二行语句的PID和第一行中由fork()创建的进程的PID一样,说明它是由fork创建出来的进程,它的PPID是15779与test.exe
一样,说明fork创建的进程,是原先进程的子进程。
此时我们就可以通过fork()
函数的返回值不同,来判断父子进程了。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{int ret = fork();if(ret < 0){perror("fork");return 1;}else if(ret == 0){ //childprintf("I am child : %d!, ret: %d\n", getpid(), ret);}else{ //fatherprintf("I am father : %d!, ret: %d\n", getpid(), ret);}sleep(1);return 0;
}
对此我们可以知道fork函数对于子进程,它的返回值为0,对于父进程,创建新的进程的PID。
父进程与子进程的关系
现在我们来总结一下父进程与子进程之间是什么样的关系
父进程通过特定的系统调用(如 fork
)创建子进程。子进程是由父进程创建出来的,它们的关系特点如下:
- 资源继承
- 子进程会继承父进程的部分资源,例如打开的文件描述符、环境变量等。
- 例如,父进程打开了一个文件用于读取,子进程也可以访问和操作这个文件。
- 进程ID关系
- 父进程有一个唯一的进程标识符(PID),子进程也有自己的 PID。
- 同时,子进程会记录其父进程的 PID(PPID)。
- 执行顺序
- 父进程和子进程的执行顺序是不确定的,由操作系统的调度器来决定。
子进程和父进程会共用代码段。