【Linux进程篇3】说白了,Linux创建进程(fork父子进程)也就那样!!!
---------------------------------------------------------------------------------------------------------------------------------
每日鸡汤:没人可以好运一生,只有努力才是一生的护身符,不放弃、不辜负。
---------------------------------------------------------------------------------------------------------------------------------
目录
前言
一:前期知识铺垫
1.1:查看/杀死进程指令
1.1.1:查看进程指令
1.1.2:杀死进程指令
1.2:获取进程PID(编号标识符)
1.2.1:while循环监视脚本
1.2.2:获取进程PID
1.3:获取进程的父进程编号标识符(PPID)
1.4:总结
二:父进程fork创建子进程
三:创建进程需要打败的四个怪兽(问题)
3.1:怪兽一:为什么fork要给子进程返回0,给父进程返回子进程的PID?
3.2:怪兽二:fork函数究竟在干什么?干了什么?
3.3:怪兽三:一个函数是如何返回2次的?如何理解?
3.4:怪兽四:一个变量怎么会有两个不同的内容?如何理解?
前言
根据前篇文章,我们已经了解了操作系统通过先描述再组织来对进程进行管理。描述进程操作系统自动生成task_struct结构体,组织进程将多个task_struct结构体对象”链起来“形成数据结构,这样对进程的管理就变成了对链表的增删改查。
一:前期知识铺垫
1.1:查看/杀死进程指令
1.1.1:查看进程指令
ps ajx:查看所有的进程;
ps ajx | grep pro:只查看名为pro的进程
ps ajx | head -1 ; ps ajx | grep pro:查看pro进程的时候将最开始的那一列名称显示出来。
多行指令并行起来【; &&】:
所以将 ps ajx | head -1 指令和 ps ajx | grep pro指令并行起来:
法一:ps ajx | head -1 && ps ajx | grep pro
法二:ps ajx | head -1 ; ps ajx | grep pro
1.1.2:杀死进程指令
但是发现有多余的进程——grep --color=auto pro;这是因为grep”关键字过滤“,要过滤名为pro的进程,前提就是将grep指令运行起来,一旦被运行起来,grep就也被加载到了内存当中了,形成进程。那么如何将它给取消(杀)掉呢?
杀死进程的两种方式:
法一:grep -v grep【ps ajx | head -1 && ps ajx | grep pro | grep -v prep】
法二:kill -9 该进程的PID编号【常用】
1.2:获取进程PID(编号标识符)
1.2.1:while循环监视脚本
while :; do echo "Hello Linux"; sleep 1; done
while循环打印Hello Linux,每间隔1秒打印一次。
每间隔1秒获取一个进程 ,用来监视进程情况。
while :; do ps ajx | head -1 ; ps ajx | grep pro | grep -v grep; echo "----------------------"; sleep 1; done
开始时没有进程,之后运行可执行程序pro进程时,立刻就出现了./pro进程了
1.2.2:获取进程PID
PID也是task_struct内部的属性之一。通过getpid函数获取该进程的PID编号
看例子:查看getpid函数用法:man getpid
pro.c文件内部
看运行情况:
1.3:获取进程的父进程编号标识符(PPID)
PPID:该进程父进程的PID。
getppid函数:获取当前进程的父进程PID。
查看getppid函数的用法:man getppid
验证情况:
pro.c文件内部:
运行情况:
那么该进程的父进程到底是什么?
查看父进程:ps ajx | head -1 ; ps ajx | grep 4901 | grep -v grep
该进程的父进程是bash进程。bash进程的PID=4901
1.4:总结
- 进程的PCB数据结构体中含有一系列属性,例如PID和PPID就是PCB数据结构体中的属性。
- 通过系统调用接口使用getpid()方法获取某一个进程的PID,使用getppid()方法来获取该进程的PPID(父进程PID)
- 发现,无论我们怎样操作进程,父进程PPID总是不变的(除了重新登陆xshell)
- 查看父进程:ps ajx | head -1 ; ps ajx | grep 21823(该进程的PPID)=> bash进程,PID = 21823,即该进程是通过bash进程创建出来的【使用fork函数】
- 每次登录xshell的时候,系统就会创建一个新的bash进程,所以每一次的bash进程的PID可能会不同=> 在终端上输入的所有进程都是bash的子进程。
二:父进程fork创建子进程
fork函数手动创建进程:
fork函数创建进程成功,会返回两个值。
pro.c文件
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
int main()
{
printf("begin:我是一个进程,pid:%d, ppid:%d\n",getpid(),getppid());
pid_t id = fork();
sleep(1);
if(id == 0)
{
//子进程
printf("子进程id = %d\n",id);
while(1)
{
printf("我是一个子进程,pid: %d, ppid: %d\n",getpid(),getppid());
sleep(1);
}
}
else if (id > 0)
{
//父进程
printf("父进程id = %d\n",id);
while(1)
{
printf("我是一个父进程,pid: %d, ppid: %d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//错误,error
}
return 0;
}
编译运行情况:
执行流,是从上往下依次进行的。
- 并且fork函数给父进程返回子进程的PID,给子进程返回0。为什么?
- fork函数到底干了什么使得一个id进入了两个循环。
- fork函数怎么能够返回两次的。为什么?
- 发现一个id变量竟然可以有两个值。为什么?
三:创建进程需要打败的四个怪兽(问题)
3.1:怪兽一:为什么fork要给子进程返回0,给父进程返回子进程的PID?
答:返回不同的返回值,是为了区分让不同的执行流执行不同的代码块。即,可以通过fork返回值明确访问哪一个子进程。
一般而言,fork之后的代码块”父子共享“。
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
int main()
{
printf("begin:我是一个进程,pid:%d, ppid:%d\n",getpid(),getppid());
pid_t id = fork();
printf("检测父进程和子进程代码共享\n");
sleep(1);
if(id == 0)
{
//子进程
printf("子进程id = %d\n",id);
while(1)
{
printf("我是一个子进程,pid: %d, ppid: %d\n",getpid(),getppid());
sleep(1);
}
}
else if (id > 0)
{
//父进程
printf("父进程id = %d\n",id);
while(1)
{
printf("我是一个父进程,pid: %d, ppid: %d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//错误,error
}
return 0;
}
若fork之后的代码父子共享,那么”检测父进程和子进程代码共享“这句话会执行两次!!!
3.2:怪兽二:fork函数究竟在干什么?干了什么?
知道:
进程 = 内核数据结构(PCB/task_struct) + 代码和数据
所以, fork创建子进程的本质:系统中多了一个进程(PCB / task_struct)。并且,通过fork创建的子进程以父进程为模板,在对应的属性(pid,ppid,...)进行一些修改来创建的一个新的进程。
fork之后,父子进程之后代码是共享的。且,在代码加载到内存中之后,代码是不可修改的,即内存中的代码是不可被修改的。
那么fork函数之后,父子代码是共享的,那么数据呢?=>问题四解决。
我们为什么要创建子进程呢?最主要的目的就是需要想办法让父与子进程执行不同的代码块(让fork具有不同的返回值)。
3.3:怪兽三:一个函数是如何返回2次的?如何理解?
首先说明,fork也是一个函数。并且因为父子进程代码是共享的。
因为父子进程都是独立的task_struct,都是可以被CPU调度运行的。
return ret; :也是代码,也是属于父与子共享的【父进程返回一个ret,子进程返回一个ret】
所以一个函数返回了2次的。
3.4:怪兽四:一个变量怎么会有两个不同的内容?如何理解?
我们已经知道通过fork函数是可以返回2次的。那么一个id变量怎么会有不同的内容?
一个学习知识点:进程是具有独立性的!!!
任何平台,进程在运行的时候,是具有独立性的【例如,关闭酷狗音乐进程,并不影响xshell进程...】,即一个进程退出崩溃不影响另一个进程。
因为父进程和子进程属于两个进程,两个进程各自具有独立性!
那么如何看待进程中【代码和数据】中的数据?答:因为在内存中的数据可能被修改,所以为了保证父子的独立性,就不能让父进程和子进程共享同一份数据!!!
父进程的数据是原有的数据,那么子进程的数据呢?
理论情况下:操作系统会单独的拷贝父进程的数据,在粘贴到另外内存中开辟的空间,作为子进程的数据!
但是,实际上数据层面会发生写时拷贝【用多少,给多少空间】(达到节省空间的目的)
步骤如下:
首先,父进程和子进程同时指向相同的代码和数据。之后父进程和子进程进行后续代码的共享。但是当子进程想对数据(共享的某一数据)进行修改时,操作系统为了保证进程的独立性,会进行拦截(将子进程要修改数据的要求进行拦截),此时操作系统将要修改的数据变量那一部分(id变量)重新在内存中开辟合适的新空间,将该变量放入新建空间中,该新建空间独属于子进程。这样就只开辟一个4字节的空间即可。子进程修改多少数据,就创建多大的空间。
写时拷贝
fork函数的 return 过程的时候,是写入吗?
- pid_t id = fork();
- 在父进程 return 时,id就是父进程的数据
- 在子进程 return 时,操作系统写时拷贝,在新空间内定义一个新的id变量【独属于子进程的id】
如果父子进程被创建好,fork()往后,谁先运行呢?
关于谁先运行,由调度器【将进程调度到CPU中】决定,是不确定的,在内存中,有许多的进程,各个进程都要进入到CPU中执行,各个进程就是竞争关系,为了避免一个进程被调用多次,而另一个进程一次也没有被调用过,这就需要有调度器(保证进程之间公平)。