原创不易,还请给个三连支持一下鸭~
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/dd4eba4d1de74bb98955e1bc125b683c.png)
一、进程的概念
一个程序文件,只是一堆待执行的代码和部分待处理的数据,它们只有被加载到内存中,然后让CPU逐条执行其代码,根据代码做出相应的动作,才形成一个真正“活的”、动态的进程。因此,进程是一个动态变化的过程,是一出有始有终的戏,而程序文件只是这一系列动作的原始蓝本,是一个静态的剧本。
总:一个程序,当运行时,那么系统中都会产生一个对应的进程,动态的概念。
二、进程的内存结构
进程的内存是一种虚拟内存,本质是对物理内存的映射。包括:内核、栈、堆、数据段、代码段 和 不可访问段。如下图:
为了更好地展示程序和进程的关系,,如下图所示:
总的来说:进程 = 代码块 + 数据块 + 进程控制块
三、进程的生老病死
如下图:
(1) 从出生看,一个进程的诞生,由其父进程调用fork()开始的。
(2) 进程刚出生时,处于task_running状态,就绪态和执行态都属于task_running, 前者在进程队列中排队,后者是占用CPU执行中。内核调用函数sched()时,就绪态会转变成执行态,所以sched()也被称为进程调度器。可以使用时间片模式或者高优先级抢占模式,亦或者两者的混合决定由谁来占用CPU。
(3) 进程处于就绪态时,可能由于某些临界资源不可得,而被置睡眠态/挂起态,称为:task_interruptible(浅睡眠) 和 task_uninterruptible(深度睡眠) 。两者的区别是,浅睡眠可以被信号唤醒,而深度睡眠不可被唤醒,。浅睡眠在要等待的资源变得可得的时候,又会被系统置为task_running状态重新排队。
(4) 当进程收到SIGSTOP 或 SIGTSTP 的信号时,状态会被置为task_stopped(暂停态)。该状态下,进程不参与cpu调度,但也不释放资源,直到收到SIGCONT信号后被重新置就绪态。当进程被追踪时(代码调试),进程的状态是task_traced,和暂停态是类似的。
(5) 进程的死亡可以有很多方式,可以是寿终正寝的正常退出,亦可被异常杀死。可能是在main函数中return,也可能是执行exit()/Exit()/_exit(),再可能是最后一个线程执行pthread_exit(),再再可能是直接被信号杀死。不管如何,最后会由监尸官(内核)发一道死亡证明( do_exit() ),将该进程置为exit_zombie状态,也叫僵尸态。
(6) 僵尸态的进程,其进程控制块里面有他的死亡信息,等待父进程进行回收。父进程可调用wait()/waitpid()来查看子进程的“死亡信息”,比如子进程是被哪个信号杀死的,死亡后的返回值是多少等等。父进程处理完这些信息后,会将尸体送进火葬场,即将子进程置为exit_dead状态(死亡态),此时系统就回收了子进程的进程控制块了(先前的其它内存资源在僵尸态时已被回收)。
值得深挖的是:如果子进程的父进程先死,那怎么办?谁来处理子进程的尸体?这时候就由父进程的父进程,也就是祖父进程回收。那祖父进程也死了呢?套娃是吧,那就由祖父的父进程回收。最后有一个祖宗是长生不死的(最少与系统共存),那就是init进程,如果某个进程的家族死光了,那最后就由祖宗回收吧。
四、Linux系统编程之进程
进程创建fork
#include <unistd.h>
pid_t fork();
返回值: >0 表示父进程,此时返回值中存放的是子进程的id号
==0 表示子进程
-1 表示失败
fork函数的特点
第一个: 一次调用,两次返回,分别表示父子进程
第二个: fork创建子进程的时候,会给子进程分配独立的内存空间,同时子进程会复制父进程的所有资源(变量,代码)
vfork函数的特点
第一个: 父子进程共享数据段
第二个: 子进程一定先于父进程运行
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main(){
pid_t pid;
int x = 0;
printf("我是fork之前的语句\n"); //在fork之前,除了变量,在子进程中都不会调用
pid = fork();
if(pid > 0){
printf("我是父进程\n");
printf("x:%d\n",x); // 0
sleep(1);
}
else if(pid == 0){
printf("我是子进程\n");
printf("x:%d\n",x); //0
sleep(1);
}
int k = 66;
printf("k:%d\n",k);
printf("我在结尾,不知道父子会不会都调用\n"); //父子进程都会调用
wait(NULL); //父进程回收子进程,如果子进程没有执行完,会阻塞
return 0;
}
可以看到,fork之前的printf由父进程调用,子进程不调用,当是父进程的变量会复制到子进程中,所以父进程和子进程都可以访问x,但是这两个x是独立的,父进程改变x不会同步到子进程,反过来亦然。
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main(){
pid_t pid;
int x = 0;
printf("我是fork之前的语句\n"); //在fork之前,除了变量,在子进程中都不会调用
// 如果想要父子进程共享x,可使用vfork()
pid = fork();
if(pid > 0){
printf("我是父进程\n");
printf("x:%d\n",x); // 0
x += 1;
printf("我是父进程,更改x后:%d\n", x);
sleep(1);
}
else if(pid == 0){
printf("我是子进程\n");
printf("x:%d\n",x); //0
x += 10;
printf("我是子进程,更改x后:%d\n", x);
sleep(1);
}
wait(NULL); //父进程回收子进程
return 0;
}
进程的结束和回收
结束进程:
exit()
_exit()
区别:
exit结束进程的时候会刷新缓冲区,但是_exit()不会刷新缓冲
正常情况下:\n和return语句都能帮我们刷新缓冲区
return和exit的区别:
区别一:return 是关键字,exit()是函数
区别二:return结束函数,返回返回值
exit()结束进程
进程的回收
#include <sys/wait.h>
pid_t wait(int *stat_loc);
返回值:成功 回收到的那个子进程的id号
失败 -1
参数(重点):stat_loc --》存放进程退出时候的状态信息
进程的退出值仅仅只是状态信息的一部分,状态信息还包含其它内容(子进程是正常退出还是异常退出,是被哪个信号弄死的)
特点:阻塞父进程,等待子进程的退出
pid_t waitpid(pid_t pid, int *stat_loc, int options);
返回值:成功 回收到的那个子进程的id号
失败 -1
参数:pid --》 < -1 回收进程组id是pid绝对值中的某个进程
比如: waitpid(-10000,) 回收进程组ID是10000的这个组里面的某个进程
== -1 回收任意一个进程
比如: waitpid(-1,);
== 0 回收本进程组中的某个进程
比如: waitpid(0)
> 0 指定回收进程id是pid的这个进程
比如: waitpid(10000,); 回收id是10000的这个进程
options --》 WNOHANG 非阻塞等待,父进程在退出的时候,如果子进程还没有退出,那么父进程不会阻塞,也不会去回收,直接退出
0 阻塞等待
补充:进程组就是多个进程组成的一个集合
获取进程的ID
// 获取当前进程的id号
pid_t getpid(void);
// 获取父进程的id
pid_t getppid(void);
system 和 exec
我们创建子进程时,一般不会在子进程中直接写丰富的代码来执行任务,而是使用system或者exec来执行其它的可执行程序。
比如:
main1.c
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(){
pid_t pid;
pid = fork();
if(pid > 0){
printf("我是父进程,我的子进程是%d\n",pid);
sleep(1);
}
else if(pid == 0){
printf("我是子进程\n");
// 注意,这里有父进程、子进程和system调用的进程,三个进程
system("./main2");
// 而使用exec函数簇时,不会产生新的进程,就只有父子进程
//execl("./main2","./main2",NULL);
sleep(1);
}
wait(NULL); //父进程回收子进程
return 0;
}
main2.c
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main(){
pid_t myid = getpid();
printf("进程%d在执行中···", myid);
return 0;
}
下面来详细讲解 system函数 和 exec 函数族的用法
system:
头文件:#include <stdlib.h>
int system(const char *command)
参数: command --》你要执行的shell命令或者可执行程序
用法一:执行shell命令
用法二:执行另外一个可执行程序
execl:
int main()
{
//execl("/bin/ls","ls","-l",NULL); //ls -l
//execl("/bin/cp","cp","1.txt","/home/gec",NULL); // cp 1.txt /home/gec
execl("/mnt/hgfs/code/hello","./hello",NULL); //./hello
}
execv:
int main()
{
//定义指针数组
//char *arg[]={"ls","-l",NULL};
//execv("/bin/ls",arg);
//char *arg[]={"cp","1.txt","/home/gec",NULL};
//execv("/bin/cp",arg);
char *arg[]={"./hello",NULL};
execv("/mnt/hgfs/code/hello",arg);
}
execle:
int main()
{
//定义指针数组
char *envp[]={"export","PATH=/mnt/hgfs/code:$PATH",NULL};
//execle("/bin/ls","ls","-l",NULL,envp);
//execle("/bin/cp","cp","1.txt","/home/gec",NULL,envp);
execle("/mnt/hgfs/code/hello","./hello",NULL,envp);
}
类似的还有:execlp、execvp、execvpe,功能差不多(有时候感觉弄这么多类似的接口真的莫名其妙)。