1. 什么是进程
在有的教材中会看到:运行起来的程序,或加载到内存的程序叫做进程。但实际上这是不够严谨的说法。
通过上一篇操作系统文章了解到:根据冯诺依曼体系结构决定,程序执行前需先加载到内存中。并且通过日常使用,会有多个程序同时打开的情况,所以可以得到一个结论:系统当中会同时存在大量的进程。 既然会同时存在大量的进程,必然是需要操作系统来管理这些进程的。那么如何管理?这就是上篇文章提到的:先描述,再组织。
在内核层面上,管理进程只把程序加载到内存是不够的。打个比方:一个人到学校门口要求进学校,但被保安拦住,这个人说是这所学校的学生,保安需要证明,这个人说我在这个学校里,保安不同意并且说:“我也在这个学校里,难道我也是学生吗?”显然如果要证明自己学生的身份,需要的是在学校系统里的姓名、学号等一系列的信息,也就是描述这个人的学生信息。
把一个可执行程序加载到内存中这不叫做一个进程,因为操作系统管理一个进程必须要做到:先描述,再组织。 为了描述一个进程,必须要有一个进程控制块:PCB(Process Control Block) 结构,在Linux中,这个结构叫做:struct task_struct{}。虽然目前我们不懂这个PCB,但至少知道,在这个结构体中应该包含着进程相关的所有属性信息。
每当一个程序加载到内存时,操作系统要为当前程序创建一个PCB结构,并且在这个结构中,一定能让我们找到当前程序的代码和数据,这就是先描述,而且结构中还会存在一个*next指针,来将所有的进程组织起来,这样就会得到一个进程链表。所以操作系统对进程的管理并不是对代码和数据的管理,而是转化成对链表的增删查改。
所以得出一个结论:进程 = PCB(内核数据结构) + 代码和数据。
Linux2.6.18源码中的struct task_struct(部分)
而所谓的进程排队,也并不是代码和数据在CPU中排队,而是存在一个调度队列
struct runqueue{},通过这个调度队列来链接进程链表中的节点,通过这个节点找到对应的代码和数据喂给CPU运行。所以进程排队本质上就是让PCB进行排队。这里再打个比方:当我们找工作时需要投递简历,简历上包含了我们的所有信息,那么在找工作的时候是我们在排队吗?其实本质上是我们的简历在排队。而这里的代码和数据就是找工作的我们,简历就是PCB。
2. task_struct
在一个
task_struct结构中会包含以下信息(具体详细信息在后面介绍):
- 标识符:描述本进程的唯一标识符,区别于其他进程。
- 状态:进程状态、退出代码、退出信号等。
- 优先级:相较于其他进程的优先级。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,和其它进程共享的内存块的指针。
- 上下文数据:进程执行时处理器的寄存器中的数据。
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记帐号等。
- 以及其他信息
3. 查看进程
要查看当前进程的标识符可以通过
getpid()函数:
通过man getpid指令来查看文档
接下来写一个简单的代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = getpid();
while(1)
{
printf("I am a process, pid:%d\n", id);
sleep(1);
}
return 0;
}
运行后就可以看到当前进程的pid:
那么该如何通过pid查看当前的进程呢?
3.1 方法一:通过/proc目录
在Linux根目录下有一个/proc目录。
进入目录内部,此时的2748480目录就是刚才运行的进程。
终止进程后,再次运行,pid也随之改变,/proc目录下所对应的目录名字也随之更改。
进入到对应的pid:2798530目录后可以看到当前进程属性,红色框表示进程自己对应的二进制可执行文件,绿色框则表示了当前进程的工作路径,所以进程启动的时候,默认工作路径就是自己可执行程序所处的路径。
如果要更改当前路径,可以通过
chdir()函数。
通过/proc目录来查看进程的方法实际上并不常用。
3.2 方法二:命令行
ps axj | head -1 ; ps axj | grep "进程名字或pid"
在下图可以看到当前进程的pid,但还有一个ppid,这就是当前进程的父进程的标识符。
在Linux系统中,增多进程是通过父进程创建子进程的方式,来让Linux系统中的进程变多的。
要查看当前进程的父进程标识符可以通过
getppid()函数。
通过man getppid指令来查看文档
通过代码来观察一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t ppid = getppid(); // 获取当前进程的父进程pid
pid_t id = getpid(); // 获取当前进程pid
while(1)
{
printf("I am a process, pid:%d, 我的父进程ppid:%d\n", id, ppid);
sleep(1);
}
return 0;
}
但当我们多运行几次后会发现,进程本身的pid是每次都在变化的,但是父进程ppid却是不变的。
这时再通过命令行来观察一下这个pid为2815421的进程是什么。
所以我们在命令行中,启动命令/程序的时候,都会变成进程,而该进程的父进程就是bash!
在Linux中,Bash (Bourne-Again SHell) 是最常用的命令行解释器(Shell),也是大多数Linux发行版的默认Shell。它的核心功能是解析用户输入的命令,与操作系统内核交互,执行脚本,并管理任务!
4. fork()函数初识
通过man fork指令来查看文档
接下来写一段代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("我是一个进程, pid:%d, ppid:%d\n", getpid(), getppid());
fork();
sleep(1);
printf("你能看到我这条消息吗?\n");
sleep(1);
return 0;
}
可以看到打印出了两条信息,这是因为调用
fork函数后会创建一个新的子进程,也就是两个执行流,所以会出现这种情况。
接下来对代码稍微修改一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("我是一个进程, pid:%d, ppid:%d\n", getpid(), getppid());
fork();
sleep(1);
printf("你能看到我这条消息吗? pid:%d, ppid:%d\n", getpid(), getppid()\n");
sleep(1);
return 0;
}
这次可以看到当前进程的pid为:2865676;而
fork之后创建的子进程pid为:2865677,父进程ppid为:2865676。
再来看一下文档中
fork的返回值:
- 调用后子进程的pid返回给父进程。
- 返回0给子进程。
- 失败返回-1,不创建子进程。
也就是
fork成功后会有两个返回值。
下面通过返回值来观察:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("我是一个进程, pid:%d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
sleep(1);
printf("你能看到我这条消息吗? pid:%d, ppid:%d, ret = %d\n", getpid(), getppid(), id\n");
sleep(1);
return 0;
}
在Linux中创建一个新的子进程,子进程的
task_struct会以父进程的task_struct为模板进行初始化。并且在fork之后默认共享父进程的代码和数据。
实际应用多进程会分流处理:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("我是一个进程, pid:%d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
while(1)
{
printf("我是子进程!pid:%d, ppid:%d, ret = %d\n", getpid(), getppid(), id);
sleep(1);
}
}
else
{
while(1)
{
printf("我是父进程!pid:%d, ppid:%d, ret = %d\n", getpid(), getppid(), id);
sleep(2);
}
}
return 0;
}
那为什么一个函数会有两个返回值呢?来看下面的图片:
当然还有一个问题,
id这个变量为什么会出现两个不同的值?这是因为发生了写时拷贝(这个具体后面文章再讲述,需要理解程序程序地址空间)。