[Linux]进程概念
文章目录
- [Linux]进程概念
- 进程的定义
- 进程和程序的关系
- Linux下查看进程
- Linux下通过系统调用获取进程标示符
- Linux下通过系统调用创建进程-fork函数使用
进程的定义
进程是程序的一个执行实例,是担当分配系统资源(CPU时间,内存)的实体。
进程和程序的关系
由编程语言编写的代码经过编译后形成的二进制程序会存储在硬盘中,当计算机启动一个程序时,会将程序的相关代码和和数据加载到内存中,供CPU来使用:
程序的代码和数据加载到内存后,操作系统就要对程序进行管理,为了更好的管理这些程序,需要对先创建相应的结构来描述这些程序,在操作系统中,用于描述程序的结构叫做进程控制块(Process Control Block,简称 PCB),Linux系统下的PCB名为task_struct,PCB中也会记录相应的代码和数据的地址,为了更好的访问这些PCB使用链式结构将其组织起来:
task_ struct内容分类如下:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息。
如果只是将程序的代码和数据加载到内存中,但是操作系统没有为其创建PCB进行管理,操作系统就不会调度它,它就无法完成程序的执行,因此进程的本质是内存中的代码和数据+进程控制块,有了PCB后,操作系统就将对进程的管理转化为了对PCB的管理,比如如果要关闭一个进程就将其PCB删除,然后对应的内存就会清空其在内存中的代码和数据:
Linux下查看进程
为了更好的在Linux操作系统上查看进程,创建源文件myprocess.c和makefile文件来创建二进制程序,
源文件中内容如下:
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("hello myprocess\n");
sleep(1);
}
return 0;
}
其中makefile的内容如下:
myprocess:myprocess.c
gcc -o myprocess myprocess.c
.PHONY:clean
clean:
rm -f myprocess
创建好以上文件并编译得到名为myprocess的二进制程序,然后在Linux下启动两个客户端,其中一个启动程序变成进程:
再另一个客户端输入ps axj | head -1 && ps axj | grep myprocess | grep -v grep
查看myprocess进程:
以上为使用指令查看进程,指令如下:
ps axj | head -1 && ps axj | grep 进程名 | grep -v 进程名
另外还可以在/proc目录下看到进程:
/proc目录是一个内存级的目录,不存在于硬盘中,目录中会有命名和pid相同的目录,该目录中会记录对应进程的task_struct,如果进程关闭了对应的目录也就删除了。
Linux下通过系统调用获取进程标示符
Linux操作系统为了唯一标识一个进程,给每个进程设置了一个进程标识符在PCB中,也就是pid。并且也提供了系统接口函数getpid来获取当前进程的pid,其介绍如下:
为了测试getpid函数修改源文件myprocess.c,内容如下:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("hello myprocess, 我的pid是%d\n", getpid());
sleep(1);
}
return 0;
}
用指令查询进程pid和查看进程执行结果:
另外Linux操作系统中还设置了父进程标识符,用于记录当前进程的父进程pid,也就是ppid,同时也提供了getppid函数来获取当前进程的ppid,ppid的介绍如下:
为了测试getppid函数修改源文件myprocess.c,内容如下:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("hello myprocess, 我的pid是%d, 我的ppid为%d\n", getpid(), getppid());
sleep(1);
}
return 0;
}
用指令查询进程pid和查看进程执行结果:
多次利用 ctrl+c
关闭进程,然后重新启动进程:
可以看出,无论进程的pid如何变化,进程的ppid都不会变化,我们尝试用指令查看这个父进程:
实际上这个这个父进程就是bash,通过如上现象我们可以得到如下结论:
- 命令行解释器(bash)本质也是一个进程。
- 命令行启动的所有程序最终都会变成进程,而该进程对应的父进程都是bash。
Linux下通过系统调用创建进程-fork函数使用
fork函数是Linux系统提供的创建子进程的系统调用。
- fork函数运行成功后,执行流会变成两个,一个是调用fork函数的父进程,另一个是fork函数创建的子进程。
- 创建的子进程会和父进程共享父进程代码和数据,子进程会执行父进程fork函数创建子进程之后的代码。
- fork函数给父进程返回子进程的pid,给创建的子进程返回0,出错返回-1。
为了测试fork函数修改源文件myprocess.c,内容如下:
#include <stdio.h>
#include <assert.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
//子进程
printf("我是子进程,我的pid是%d, 我的ppid是%d\n", getpid(), getppid());
sleep(2);
}
else if (id > 0)
{
//父进程
printf("我是父进程,我的pid是%d, 我的ppid是%d\n", getpid(), getppid());
sleep(3);
}
else
{
//fork函数出错
assert(1);
}
return 0;
}
说明:
- fork函数所需要的头文件是
unistd.h
。 - 使用条件判断来控制父子进程执行不同的代码。
用指令查询进程和查看进程执行结果:
fork函数的原理
进程的本质是PCB+内存中的代码和数据,由于fork函数创建的子进程是和父进程共享代码和数据的,因此fork函数创建子进程的原理是创建一个PCB给子进程,该PCB中大部分数据是和父进程相同的,并且指向同一份代码和数据:
进程独立性在fork中的体现
首先给出如下定理:进程之间是相互独立的,一个进程的任何操作都不会影响其他进程。
在使用fork函数创建子进程进程之间的独立性也能得到保证,为了验证独立性修改源文件myprocess.c,内容如下:
#include <stdio.h>
#include <assert.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
//子进程
printf("我是子进程,我的pid是%d, 我的ppid是%d\n", getpid(), getppid());
sleep(20);
printf("我是子进程,我的pid是%d, 我的ppid是%d\n", getpid(), getppid());
printf("我是子进程,我已经关闭了\n");
}
else if (id > 0)
{
//父进程
printf("我是父进程,我的pid是%d, 我的ppid是%d\n", getpid(), getppid());
sleep(3);
printf("我是父进程,我已经关闭了\n");
}
else
{
//fork函数出错
assert(1);
}
return 0;
}
用指令查询进程和查看进程执行结果:
开始时,父子进程一起执行:
父进程关闭,子进程正常运行:
最后子进程关闭:
由以上测试可以看出,父进程的关闭不影响子进程正常执行,保证了一定的独立性。另外由于代码是只读的,父进程无法通过修改代码来影响子进程,而数据的修改会触发写时拷贝机制,保证了一定的独立性。
为了观察写时拷贝现象,修改源文件myprocess.c,内容如下:
#include <stdio.h>
#include <assert.h>
#include <unistd.h>
int main()
{
int a = 0;
pid_t id = fork();
if (id == 0)
{
//子进程
printf("我是子进程,我的pid是%d, 我的ppid是%d, a:%d, &a:%p\n", getpid(), getppid(), a, &a);
sleep(5);
printf("我是子进程,我的pid是%d, 我的ppid是%d, a:%d, &a:%p\n", getpid(), getppid(), a, &a);
}
else if (id > 0)
{
//父进程
printf("我是父进程,我的pid是%d, 我的ppid是%d, a:%d, &a:%p\n", getpid(), getppid(), a, &a);
a = 666;
printf("我是父进程,我的pid是%d, 我的ppid是%d, a:%d, &a:%p\n", getpid(), getppid(), a, &a);
sleep(3);
printf("我是父进程,我已经关闭\n");
}
else
{
//fork函数出错
assert(1);
}
return 0;
}
查看进程执行结果:
观察现象可以发现,父进程修改a的值后,子进程的a的值并没有改变,但是父进程和子进程的a变量的地址是相同,这就是发生了写时拷贝造成的现象。
fork函数返回两个返回值的原理
由于fork创建的子进程和父进程共享代码和数据,并且fork函数也是父进程的代码的一部分,因此父进程完成子进程的创建后,子进程也会执行fork函数创建子进程后续的剩余代码,其中就包括fork函数中return返回的部分,因此父进程执行了return部分,子进程也执行了return部分,造成fork函数返回两个返回值的现象: