【Linux】进程概念
1. 基本概念
常见的教材观点:
- 课本概念:程序的一个执行实例,正在执行的程序。加载到内存中的程序
- 内核观点:担当分配系统资源(CPU时间,内存)的实体。正在运行的程序
但是这里我们理解为将一个一个或者多个通过编译形成的.exe文件加载到内存中时,这个是时候我们称之为进程。
2. 描述进程-PCB
我们上一章节也是讲过了,我们要对某个结构化的数据进行管理该怎么做啊——先描述,在组织。在内存中会不会有很多个被加载到内存中的的程序啊,那么操作系统要不要多这些程序进行管理啊。肯定是要的。所以进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,课本上称之为PCB(process control block)
,Linux
操作系统下的PCB
是: task_struct
。
所以往后对进程的管理就变成了对描述这个进程的PCB进行管理了。而形成PCB只是描述的一个过程,操作系统要进行管理还需要进行组织起来,所以PCB中包含一个指针字段,通过链表进行管理。所以往后对进程的管理就变成了对PCB的增删改查的操作。
所以为什么程序加载到内存中,变成进程后,要给每一个进程形成一个PCB呢
因为操作系统可以更好的对进程进行管理。
所以综上我们可以总结以下什么是进程:
- 进程=内核PCB对象(内核数据结构)+ 数据。
所以这里输出一个结论——未来,对进程的所有操作都只和进程的PCB有关,和进程的的可执行程序无关。所以如果愿意,可以把PCB放到任何一个数据结构里面。
2.1 task_struct-PCB的一种
我们上面讲解的PCB是一种通用的概念,无论是针对linux还Windows都是如此,但是每种操作系统中对应的名称叫法是不一样的,这里我们就拿Linux来讲。在Linux中描述进程的结构体叫做task_struct。
2.2 task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。CPU中有一个pc指针记录当前执行位置的下一个位置。所以我们之前执行的的判断,循环,函数跳转,本质就是修改pc指针。pc指向哪一个进程的代码,就表示哪一个进程被调度了。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
3. 组织进程
3.1 查看进程
可以使用 ps axj
这个命令看到的全部都是进程
我们也可以做一个实验,先写这段代码。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
sleep(1);
}
return 0;
}
将这个程序运行起来,执行以下指令ps axj | head -1 && ps axj | grep text
- ps axj是显示进程信息
- | 这个是管道
- && 并且
- grep text 过滤text
- 但是这里会发现有两个,这是因为我们执行grep,这个也是一个指令,运行起来也是一个程序所以也会进行查询,也就是说几乎所有的独立指令,就是程序,运行起来就是一个进程,如果需要进行过滤可以加上-v选项,反向匹配。就可以执行
ps axj | head -1 && ps axj | grep text | grep -v grep
- 所以我们要看到一个进程的产生,也要看到一个进程的消亡,所以我们可以执行以下循环查询操作:
while :; do ps axj | head -1 && ps axj | grep text | grep -v grep; sleep 1; done
4. 通过系统调用获取进程标示符
通过上述我们可以知道PCB是描述一个进程的结构化数据,也就是tesk_struct结构体,所以里面必然包含了各种有关进程的属性数据。那么这些属性数据属于操作系统的范畴吗?答案是肯定的,既然PCB是描述一个进程的,进程是由操作系统进行管理的,所以PCB中的属性数据也一定是由操作系统进行管理的。所以如果想要拿到PCB中的属性数据就必然需要调用系统调用接口进行获取。
4.1 获取进程标识符
- 系统调用接口
二号手册是专门的系统调用接口函数
一般在Linux下,普通的进程都有它发父进程,顾名思义就是,普通进程是由父进程创建出来的。至于怎么创建后面会讲解。所以我想说的就是,除了可以获取进程的进程标识符(id),从上面的系统调用接口我们也可以发现有两个系统调用接口,第一个是获取当前进程的id,第二个就是获取当前进程的父进程的id。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = getpid();
pid_t fid = getppid();
while (1)
{
printf("I am process! pid: %d, ppid %d\n", id, fid);
sleep(1);
}
return 0;
}
这里我们可以查看一下父进程是谁
这里会发现是bash,所以我们在命令行启动的进程都是bash的子进程。bash是什么,bash就是我们的命令行解释器。
我们查看进程标识符的方法还有另外一种,进程的信息可以通过 /proc 系统文件夹查看,
- 我们所看到的这些以数字命名的目录就是一个个的进程,也就是说,每创建一个进程,都会由一个对应的使用进程标识符为名字的目录来存放该进程的所有数据。这其中有两个数据我们是比较关心的,也就是下面高亮部分。cwd和exe
- 这里如果我们在运行期间把text可执行程序删了会发什么情况呢?
这里高亮变红色了,并且提示了delete删除的提示。这也进一步证明了这个exe就是我们的可执行程序。但是这里我们有一个疑问,为什么我们把text可执行程序删除了,这个可执行程序还在运行。首先我们要明白的是可执行程序运行时在内存中的,而在可执行程序没有运行起来之前是存放在磁盘上的,从磁盘到内存中是通过拷贝过去的,也就是说我们现在运行的可执行程序是在没有删除text可知程序之前拷贝过来的,而我们删除的是磁盘上的。这也说明了,一个可执行程序加载到内存中比变成进程是与磁盘上的可执行程序已经是没有关系了。
而至于cwd它的全称是current working directory
当前工作目录。他记录的是当前进程所处的工作目录。为什么我们运行程序到时候生成可执行程序会创建在当前目录下与源文件在同一个目录,以及我们在执行fopen的时候打开文件时只需要写上打开是文件名而不是写上绝对路径他就会自动的在当前目录下查找。这都是因为由这个cwd。当我们执行可执行程序的时候,在这一瞬间被加载到内存中形成进程,也就是形成了cwd进程工作目录,这个默认是与源文件保持在同一个目录下的。所以在执行有关路径有关的操作都会使用cwd里面的内容。
4.2 更改工作目录chdir
- chdir更改工作目录系统调用
参数——更改的新路径,返回值0就是成功,-1就是失败。
这个时候我们就可以在代码中加上这个系统调用,更改指定的工作目录。
chdir("/home/chuyang");
5 通过系统调用创建进程-fork初识
5.1 接口认识
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("befor fork , I am process pid: %d!, ppid: %d\n", getpid(), getppid());
int ret = fork();
printf("after fork , I am process pid: %d!, ppid: %d\n", getpid(),getppid());
return 0;
}
从上面的现象我们首先可以发现的是after这条信息打印了两条,并且有一条是和befor是一样的,也就是说在fork之后形成了两个分支,一个分支是fork出来的,另一个是原来的。这里我们叫原来的是父进程,创建出来的是子进程。并且父进程和子进程是代码共享的。
5.2 返回值分析
成功的话就是将子进程的pid返回给父进程,将0返回给子进程。失败-1被返回错误码被设置。
我们也可将返回值打印以下。
所以有了这个特性,我们就可以通过返回值,让父子进程执行不同的代码块。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
if (ret == 0)
{
while (1)
{
sleep(1);
printf("after fork 我是子进程, I am process pid: %d, ppid: %d, return value: %d\n", getpid(), getppid(), ret);
}
}
else
{
while (1)
{
sleep(1);
printf("after fork 我是父进程, I am process pid: %d, ppid: %d, return value: %d\n", getpid(), getppid(), ret);
}
}
return 0;
}
1. 为什么是给父进程返回子进程的pid,给子进程返回的是0
首先子进程是由父进程进行创建的,也就是说明,父进程可以创建多个子进程,父进程与子进程的比率是1比n的。那么父进程后期是可以对子进程进程控制的,pid是具有唯一性的也就是唯一标识一个进程。所以如果父进程想要对子进程进行管理就必然需要知道子进程的pid。而子进程只需要关心自己是否被成功创建了,并且它的父亲也只有一个所以也不需要什么返回值来做特殊处理。
2. fork函数为什么返回两次
我们认为一个函数执行到最后,执行return的时候,这个函数的核心逻辑已经执行完毕了。而fork本身就是函数,而且函数内部也是由返回值的,在返回之前必定是需要执行很多的代码逻辑的。所以在fork的return之前一定是已经把父子进程都创建好了,那么既然都创建好了,道理return的时候,肯定是有两个返回值的。
3. 从上面的结果看来,为什么同一个变量既可以等于0,也可以大于0
这里我们输出一个结论:进程(任意,包括父子)之间是具有独立性的,是互不受影响的。前面我们也说过了,父子进程的代码和数据都是共享的。但是这个前提是父子进程都并未对数据进行修改。如果父子中其中一个要对数据进行修改,而操作系统又需要保证进程之间是互不影响的独立的,那么就必然会给要修改的数据进行重新开辟一块空间,并把原来的数据进行拷贝一份,让其中一个进程不对原来的数进行修改而是对这块新开辟的内存进行写入或者修改,这个叫做写时拷贝。而返回的操作其就是写入的操作。而操作系统底层中是可以实现同一个变量表示不同的内存的。具体的细节等我们将地址空间的时候会进行讲解。这里我们就简单的认识同一个变量可以表示不同的内存。
所以到这里我们创建进程的方法有两种了,第一种就是我们将编译好的可执行程序在命令行解释器里执行形成进程。第二种就是使用系统调用fork手动创建进程。