2024年C C++最全进程管理的概念_进程管理包括什么,20道高频面试题(含答案)

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

我们首先要写一个进程将其运行起来:

#include<stdio.h>
#include<unistd.h>
int main()
{

	while (1) {
		printf("hello\n");
		sleep(1);
	}

	return 0;
}

在Linux下我们通过ps(process)指令可以查看

[sjj@VM-20-15-centos 2022_3_20]$ ps axj | grep "myproc"

我们可以看到有两个进程

下面那个进程就是命令行的进程,我们不必太关心,上面那个进程就是我们写的程序

我们可以更进一步的把属性名字给打出来:

[sjj@VM-20-15-centos 2022_3_20]$ ps axj | head -1 && ps axj | grep "myproc"

曾经我们的所有的启动程序的过程,本质都是在系统上创建进程!

对比学习:进程VS程序

所以:进程=程序文件内容+与进程相关的数据结构, 在Linux下就是task_struct,它是由操作系统帮我们创建的,然后由于每个结构体都有进程的所有属性数据,就可以用双链表来将各个进程组织起来,这样一来操作系统就不用关心加载到内存中的代码和数据了,只需要关心PCB里面的属性信息就可以了,最核心的就是拿到双链表的头指针,就可以访问到所有的PCB结点,对于进程的管理也就可以变成对于链表数据结构的增删查改了!!!

例如:刚刚打开一个程序,创建了一个进程,将该程序加载到内存之中,操作系统就会为之创建相应的PCB,先描述了PCB,再将这个PCB结点尾插到双链表中,组织起来。当一个进程结束时,操作系统同样通过头指针找到该结点将其从双链表中删除,并保持原双链表结构不变

我们往上提升一个角度,站在CPU的角度来看待内存+OS,由于进程要运行起来,必需要CPU资源,操作系统将要运行的进程进行排队,组织成了一个**运行队列,**只需要将进程的核心PCB(Linux中是task_struct)不断的尾插到队列中,但task_struct里面有指针依旧指向该进程的代码和数据,所以只需要排队,CPU就能找到该进程的所有属性,最后CPU就能完成进程!

**总结:**有了进程控制块PCB,所有的进程管理任务与进程对应的程序代码数据毫无关系!而是与进程对应的内核(操作系统)创建的该进程的PCB强相关!

通过系统调用创建进程——fork

先来段代码观察现象:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
	fork();
	printf("hello\n");
	sleep(1);
	return 0;
}

执行效果:

ps:在vim中不退出查看man手册

先Esc进入命令模式,再shift+:进入底行模式,输入 !man fork就可以了

打开man手册:

再来看一段小代码:

#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
	fork();
	cout << "hello proc:" << getpid() << " " << "hello parent:" << getppid() << endl;
	sleep(1);
	return 0;
}

我们可以看到有行结果:

其中的3086就是上一命令行的PID,就是./myproc的进程

通过命令我们查询验证一下3086是谁

[sjj@VM-20-15-centos 2022_3_24]$ ps axj | head -1 && ps axj | grep 3086

bash创建了子进程,子进程又创建了子进程如此往复

为什么会有两行打印的原因就是:

很显然这在C/C++中是不存在这种现象的,它既然能跑两行,就证明了它是两个进程,这从打印的结果也可以看出来

那么如何理解fork创建子进程呢?

1、我们一般创建进程都是命令行上敲 ./程序或者run command,fork:在操作系统上面而言,上面的创建进程没有差别!

2、fork创建了一个进程,本质就是系统里面多了一个进程(进程就是与代码相关的数据结构PCB+代码和数据)

3、这里的数据结构就是task_struct,那么代码和数据呢?

我们只是fork了,创建了子进程,但是子进程对应的代码和数据呢?

答案:就是子进程在默认情况下,会继承父进程的代码和数据结构,新创建的子进程的内核数据结构task_struct也会以父进程为模板,去初始化子进程的task_struct

4、fork之后,子进程和父进程的代码和数据是共享的

5、代码是不可以被修改的,所以父子代码只有一份

6、对于数据来说,默认情况也是共享的,不过需要考虑修改的情况!

数据是通过写时拷贝,来完成进程数据的独立性

写时拷贝

写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。

只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。

如果父子进程创建时拷贝两份数据,那么fork函数的效率就会大打折扣,而且并不是所有的数据都要写入修改的

7、进程是具有独立性的!(类比Windows中各个进程打开都可以使用,而且互不影响)

代码共享和写时拷贝都是为了保证进程的独立性!

我们创建的子进程,就是为了和父进程干一样的事情?

一般是没有意义的!!!我们还是要让父进程和子进程做不一样的事情!!!

怎么让父进程和子进程做不一样的事情?

我们是通过fork的返回值来实现的

如何理解有两个返回值?

返回值也是数据,return的时候也会被写入,发生写时拷贝!

如何理解有两个返回值的设置?

一般来说父进程只有一个,而子进程有n个,父:子=1:n,父进程想要找到子进程,必须要拿到该子进程的pid(唯一表示符),从而达到控制子进程的目的,而子进程不需要找到父亲,因为父进程只有一个(可以利用getppid函数获取父进程的pid),直接返回0

我们可以通过if else分流来让父子做不一样的事情

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
int main()
{
	pid_t id = fork();//当做int
	if (id == 0) {
		//child
		while (1) {
			cout << "I am child pid:" << getpid() << " ppid: " << getppid() << endl;
			sleep(1);
		}
	}
	else if (id > 0) {
		//father
		while (1) {
			cout << "I am father pid:" << getpid() << " ppid: " << getppid() << endl;
			sleep(2);
		}
	}
	else {
		//error
	}
	sleep(1);
	return 0;
}

结果展示:

注意:父子进程并不是交替出现的!使用fork之后,先后运行次序由CPU中的调度器决定

进程状态

进程状态信息在哪里呢?

答案:task_struct(PCB)里面

进程状态的意义?

方便操作系统快速判断进程,完成特定的功能,比如调度。

具体状态:

①R运行状态(running):运行状态是不一定要占用CPU的,也是可以在运行队列里面的!

②S睡眠状态(sleeping): 意味着进程在等待事件完成,这里的睡眠有时候也叫做可中断睡眠 。(浅度睡眠)

**③D磁盘休眠状态(Disk sleep):在这个状态的进程通常会等待IO的结束,有时候也叫不可中断睡眠状态。(**深度睡眠)

**注意:**进程如果处于D状态,不可被杀掉

S和D状态都是当我们要完成某任务,条件不具备,需要进程进行某种等待

注意:千万不要认为进程只会等待CPU资源,当进程在等待CPU资源的时候就会在等等待队列中排队,当进程需要其他资源时候,就会在等待队列里面排队!

动态演示:如何在等待队列和运行队列间切换

  详细解读:当它所需要的的资源(外设)空闲时,操作系统就会把它的S状态或者D状态设置为R状态,然后从等待队列里面挑选出来,尾插到运行队列里面,等待CPU资源,等CPU资源就绪后,他就可以通过CPU调用该进程所需要的其他资源;当某一进程在运行队列中等待某种资源,CPU就会修改其状态,从R状态修改为S,并将该进程的PCB尾插到等待队列中进行等待

所谓的进程,在运行的时候,有可能因为运行的需要,可以在不同的队列里面!!在不同的队列里,所处的状态是不一样的!

另外我们把运行队列放到等待队列中就叫做挂起,从上层看就是阻塞状态!从等待队列放到运行队列就叫做唤醒!

④T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

⑤X死亡状态(dead):X状态是回收资源,就是进程相关的数据结构+该进程的代码和数据,X状态一般很难看到

⑥僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程

僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。

所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

为什么会有僵尸状态?

答案:为了辨别退出死亡的原因!(进程退出的信息是数据,就存储在PCB里面)

验证这些状态

写一个死循环

#include<iostream>
using namespace std;
int main()
{
	while (1);
	return 0;
}

增加一个命令窗口,查看进程:

[sjj@VM-20-15-centos 2022_3_25]$ ps axj | head -1 && ps axj | grep -v grep | grep myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
23368 24691 24691 23368 pts/0    24691 R+    1001   0:56 ./myproc

确实我们可以看到R+的状态!

补充:

带个 + 表示在前台运行,什么都不带就是后台进程,后台进程的特点就是可以在正在跑的命令行上敲击指令,也是可以运行的,而前台运行敲击指令不会运行

我们可以加个**&**,让程序在后台运行,例如:

[sjj@VM-20-15-centos 2022_3_25]$ ./myproc &

fg 命令可以把后台命令提到前台,方便你ctrl+c将其关掉

我们再来看一个奇怪的现象:

#include<iostream>
using namespace std;
int main()
{
	while (1)
	{
		cout << "hello" << endl;
	}
	return 0;
}

为什么大多数都是S+状态呢?偶尔出现R+状态?

原因:我们进行打印的时候,是往显示器上打印,显示器是外设,IO等待外设就绪是要花时间的,而CPU切换运行是非常快的(远远超过人肉眼能看到的),所以我们看到大部分是S状态。

我们可以用kill命令来杀掉进程,先用-l选项来查看有哪些信号:

[sjj@VM-20-15-centos 2022_3_25]$ kill -l

标记出了一些常用信号选项。

用19号信号杀掉进程!

我们再次查看进程就可以看到T状态的进程了!

我们此时让用18号信号让进程继续运行:

[sjj@VM-20-15-centos 2022_3_25]$ kill -18 28587

再次查看进程:

我们发现是S状态了,不是S+状态了,再次kill - 19 杀不掉了,此时要用kill -9 来终止进程

僵尸进程

如果没有人来检测或者回收父进程,那么该子进程就进入僵尸状态

如何查看?来一段简单的代码

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//child
		while (1) {
			cout << "I am child running!" << endl;
			sleep(2);
		}
	}
	else 
	{
		//father
		cout << "father do nothing" << endl;
		sleep(50);//在50秒内手动杀掉子进程,观察现象
	}
	return 0;
}

再来个监控命令行脚本:

while :; do ps axj |head -1 && ps axj | grep myproc | grep -v grep; sleep 1; echo "###############"; done

我们运行起来就可以观察啦

孤儿进程

父进程提前退出(exit),子进程就被称为孤儿进程,孤儿进程会被1号进程init(操作系统)领养,当然要有init进程回收

我们再来小改一下代码:

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
using namespace std;
int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//child
		while (1) {
			cout << "I am child running!" << endl;
			sleep(2);
		}
	}
	else 
	{
		//father
		cout << "father do nothing" << endl;
		sleep(10);
		exit(1);//将父进程给退出
	}
	return 0;
}

结果展示:

进程优先级

基本概念

为什么会有优先级?

答案:资源太少!本质是分配资源的一种方式!

查看优先级的指令:ps -l

我们先写一段小小的进程代码:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
  while(1)
  {
    printf("I am a process! pid:%d  ppid:%d\n",getpid(),getppid());
    sleep(1);
  }
  return 0;
}

查看结果:

我们很容易注意到几个重要的信息:

①UID : 代表执行者的身份

②PID : 代表这个进程的代号

③PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号

④PRI :代表这个进程可被执行的优先级,其值越小越早被执行

⑤NI :代表这个进程的nice值

PRI 和 NI

①PRI就是进程的优先级,通俗点就是被CPU执行的先后顺序,此值越小,进程的优先级又高

②NI就是nice值,叫做优先级的修正数据

③计算公式:PRI(new)=PRI(old)+nice

④调整进程优先级,在Linux下就是调整nice值

⑤nice值取值-20到19,共40个级别

PRI 对比 NI

①需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。

②可以理解nice值是进程优先级的修正修正数据

调整优先级

第一种方法:系统提供的调整优先级的接口(一般用的非常少——百度)

第二种方法:renice命令调整(不推荐)

第三种:top命令调整(常用)

先按top,进入top后,按r,输入进程PID,再输入nice值

修改结果:

注意:

1、当你修改的nice值超过规定范围,系统默认为最大值或者最小值

2、有可能你修改的太过于频繁了之后,系统不要你修改了,我们需要提升临时权限,输入sudo top再次进行修改

3、我们多次修改nice值,但是每次PRI(old)都是从80开始的,无论你怎么改nice值为什么是一个相对比较小的范围呢?

答案:优先级设置只是一个相对的的优先级,没有绝对的优先级,否则会出现很严重的进程“饥饿问题”(就是某个进程长时间得不到资源),我们的调度器:是尽可能均衡的让每个进程都能享受到CPU资源的

其他概念

①竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级

②独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰

并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行(某一时刻)

并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发(某一时间段)

环境变量

环境变量的基本概念

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性

说的简单点:

语言上面定义变量本质是在内存中开辟空间(有名字),不要去质疑OS开辟空间的能力!
环境变量本质是OS在内存/磁盘文件中开辟的空间,用来保存系统相关的数据! !

常见的环境变量

PATH : 指定命令的搜索路径

HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)

SHELL : 当前Shell,它的值通常是/bin/bash。

问题引出:我们敲的命令,工具,程序等本质都是一个可执行程序。那么为什么我自己写的程序要加上点斜杠 **.**才能运行起来,而系统默认的一些命令(ls,pwd,cd等),直接敲击便可以执行?

./点斜杠的实质是在帮系统确认文件在当前目录,或者说找到该文件的存放路径!

为什么系统的命令不用带路径呢?

因为有环境变量的存在!!!

查看环境变量的方法
echo $NAME//NAME:你的环境变量名

例如:

[sjj@VM-20-15-centos 2022_3_30]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/sjj/.local/bin:/home/sjj/bin

这就是为什么系统不带路径就能找到文件的原因了,因为有了环境变量,以冒号为分割,从前往后找,系统依次搜索文件所在路径,如果在当前找到了,就不用往后面的路径再寻找了。这样也可以说明,我们平时安装软件,也就是把对应的命令添加到系统环境的路径下面!

我们可不可以让自己写的程序也不带点斜杠运行呢?

方法一:把你写的程序路径,拷贝到环境变量中(但是我们强烈不推荐这样做,会污染系统的环境变量)

[sjj@VM-20-15-centos 2022_3_31]$ sudo cp myproc /usr/bin

方法二:使用export命令,将本地变量导成环境变量

格式:export PATH=$PATH:命令所在路径

测试结果:

注意: 当然这样修改只在本次登录有效,你退出登录,下次再次登录就没效了。如果想要永久使用,需要修改系统的配置文件!

我们还可以查看其它的环境变量:

这就是为什么我们每次登陆机器的时候,我们都是直接在用户家目录下,原因就是HOME环境变量的存在,我们每次登陆,系统都会默认打开这个路径。

我们可以查看更多的环境变量:使用 env 命令

总结:系统有这些环境变量,保证系统运行的相关状态信息,能更好的让我们运行、使用程序,每一种环境变量承担着不同的职责!

和环境变量相关的命令

  1. echo: 显示某个环境变量值

  2. export: 设置一个新的环境变量

  3. env: 显示所有环境变量

  4. unset: 清除环境变量

  5. set: 显示本地定义的shell变量和环境变量

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

lbmhlaQ,shadow_50,text_Q1NETiBASGVybyAyMDIx,size_20,color_FFFFFF,t_70,g_se,x_16)

这就是为什么我们每次登陆机器的时候,我们都是直接在用户家目录下,原因就是HOME环境变量的存在,我们每次登陆,系统都会默认打开这个路径。

我们可以查看更多的环境变量:使用 env 命令

总结:系统有这些环境变量,保证系统运行的相关状态信息,能更好的让我们运行、使用程序,每一种环境变量承担着不同的职责!

和环境变量相关的命令

  1. echo: 显示某个环境变量值

  2. export: 设置一个新的环境变量

  3. env: 显示所有环境变量

  4. unset: 清除环境变量

  5. set: 显示本地定义的shell变量和环境变量

[外链图片转存中…(img-us4EVJsv-1715543395822)]
[外链图片转存中…(img-TxI6PRxb-1715543395822)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值