文章目录
- 一.进程前言
- 1.冯诺依曼体系结构
- 2.操作系统
- 二.进程相关概念
- 1.PCB
- 2.查看进程标识符
- 3.父与子进程
- 三.进程状态
- 1.状态类别
- 1).运行
- 2).阻塞
- 3).挂起
- 2.Linux下的状态
- 1).R(running)
- 2).S(Sleeping)
- 3).D(disk sleeping)
- 4).T(stopped)
- 5).t(tracing stopped)
- 6).Z(僵尸进程)
- 7).孤儿进程
- 四.进程优先级
- 1.查看进程优先级
- 2.修改进程优先级
- 五.环境变量
- 1.相关指令
- 2.基本的环境变量的理解
- 3.三个命令行参数
- 1).int argc
- 2).char *argv[ ]
- 3).char *envp[ ]
- 3.举例测试
- 六.进程地址空间
一.进程前言
1.冯诺依曼体系结构
我们现代的大部分计算机任然遵守着冯诺依曼体系结构(如上图)。
我们去使用软件的时候,数据从输入设备流入我们的存储器(内存),再通过我们的中央处理器进行运算,最终数据从输出设备流出。这是一个典型计算机的工作过程。而了解这些才能更好的理解我们的进程相关概念。
2.操作系统
操作系统(OS)是管理计算机硬件与软件资源的系统软件,它为计算机的各个部件提供基础的操作环境和服务。作为计算机硬件和用户之间的桥梁,操作系统负责协调和管理硬件资源,确保各项任务能够高效有序地执行。
如何理解操作系统的沟通功能呢?
底层硬件通过中断或直接内存访问将数据传递给操作系统,我们的操作系统将底层硬件的数据和用户输入产生的数据进行整合,
然后统一通过 如链表,等数据结构 进行统一的管理
操作系统的工作就是 接收——描述——整合——管理 的过程
二.进程相关概念
1.PCB
进程包括程序代码本身,还包含执行程序所需的资源、状态信息和控制信息。(程序的执行实例)
每个进程的详细信息被保存在进程控制块(PCB,Process Control Block)。在Linux操作系统下,PCB是task_struct。而task_struct是一个链表的数据结构,可以便于管理进程。
那么操作系统是如何管理和运行我们的进程的呢?
1.运行一个进程的时候,操作系统会在RAM(内存)上开辟一个空间去存储进程的PCB。
2.每开一个新的进程就会在这条链表上继续穿插下去
PCB的内容:
1.进程标识符:用来唯一标识一个进程
2.进程状态:就绪,终止,阻塞…
3.程序计数器:保存当下运行的指令
4.寄存器内容(上下文保护):存储寄存器内容,便于下次调度时恢复
5.内存管理信息:记录进程的地址空间信息
6.调度信息:优先级,调度队列的位置(指针),时间片…
7.I/O状态:该进程是否正在I/O输入或输出。
8.文件管理信息:打开的文件列表以及相应的访问权限和操作。
9.父进程和子进程信息:每个进程都有父进程,或者有子进程。
10.信号信息:操作系统会使用信号来通知进程某些事件的发生(如终止、暂停等)
11.时间信息:程序运行的时间等等。
12.进程通信信息:进程间通信(IPC)相关的信息,如管道、消息队列、共享内存等的状态,记录进程间如何交换数据。
加深了我们对“Linux下一切皆文件”的理解
2.查看进程标识符
我们可以在Linux下的/proc文件夹查看我们进程相关的信息。
而文件夹的 ID 就是PCB下的唯一标识符。
也可以通过ps(process status)指令来查看我们的进程
指令 | 作用 |
---|---|
-e | 显示系统中的所有进程(包括其他用户的进程) |
-A | 与 -e 类似,但更为全面,显示所有进程 |
-ef | 使用“全格式”输出,显示更多详细信息,如父进程 PID(PPID)、启动时间等。 |
-u | 显示特定用户的进程,可以替换 username 为具体的用户名。 |
-aux | 显示所有用户的进程(不仅是当前用户的进程)。进程的用户/所有者,没有控制终端的进程(例如守护进程)。 |
在这个进程列表之中,PPID和PID分别代表父进程ID和自己的进程ID。
3.父与子进程
在Linux下一个进程是可以创建另一个子进程的
创建者就是父进程,被创建者就是子进程。
初始fork(),:
fork是一个系统调用接口,可以创建一个子进程。
他有两个返回值!!!
在父亲进程中返回子进程ID
在子进程中返回0
eg:
1 #include <iostream>
2 #include <unistd.h>
3 using namespace std;
4 int main()
5 {
6 pid_t id = fork();
7 if(id==0)
8 {
9 while(1)
10 {
11 cout<<"我是子 ID:"<< getpid()<<"我的ppid:"<<getppid()<<endl;
12 sleep(1);
13 }
14 }
15 else
16 {
17 while(1)
18 {
19 cout<<"我是父 ID:"<<getpid()<<"我的ppid:"<<getppid()<<endl;
20 sleep(2);
21 }
22 }
23 return 0;
24 }
三.进程状态
1.状态类别
状态共有
1.就绪 2.运行 3.阻塞 4.挂起 5.终止
就绪与运行十分相似
这边着重区分运行,阻塞,挂起。
1).运行
运行状态不仅是当前CPU调度的进程的状态,运行队列中的的进程都是运行状态。
从操作系统知道,操作系统对进程的管理是通过链表task_struct进行的。而我们的CPU只有单核或多核(远远小于进程的数量),那么操作系统可以通过何种方式使得进程可以流畅的运行呢?
运行队列就可以做到,操作系统创建一个运行队列,将进程的PCB通过链表连接起来,再设定一个时间片,每一个进程在时间片规定的时间内完成相应的工作,超出时间片就将CPU等各种资源交给下一个进程,如此往复(因为时间片的时间非常的短,所以我们肉眼感受不到进程的切换)
2).阻塞
CPU访问内存的速度是要远远快于外存的
当进程访问我们的外设进行I/O(输入/输出)时我们的运行队列该如何处理呢?总不能等着进程I/O浪费资源吧,所以操作系统衍生了阻塞这一状态,它可以将当前正在I/O的进程移出运行队列,并移入我们的等待队列(wait_list),CPU释放I/O指令给外设,使我们的进程在等待队列中排队,等到该外设空闲时进行I/O,I/O结束后又回到我们的运行队列中来。
3).挂起
PCB是一直存放在我们的内存上的,如果我们阻塞的进程很多很多,PCB是否会过多的占用内存空间呢?是的!
所以操作系统有衍生了挂起状态(它是操作系统为节省内存资源而采取的特殊状态),当进程还未调用资源的时候,为了防止PCB过多的占用内存
操作系统会将PCB暂时存放在硬盘上,这样等轮到该进程调度资源的时候,再将其加载到内存上去。节省空间。有两种情况会引起挂起的出现:
1.阻塞状态
2.就绪状态(PCB刚加载好就被挂起)
2.Linux下的状态
Linux下的状态又有细微的差别,在Linux源码下对于进程的状态有如下的定义
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
+号代表前台程序
1).R(running)
与运行状态相同
1 #include <iostream>
2 int main()
3 {
4 while(1)
5 {
6
7 }
8 return 0;
9 }
2).S(Sleeping)
与阻塞状态相似
1 #include <iostream>
2 #include <unistd.h>
3 using namespace std;
4 int main()
5 {
6 while(1)
7 {
8 cout<<"我在打印,对硬件进行I/O"<<endl;
9 sleep(1);
10 }
11 return 0;
12 }
3).D(disk sleeping)
深度睡眠状态,在Linux下我们通过操作系统可以杀掉一个进程。如果我的进程正在进行高强度的I/O呢,这时杀掉进程就会导致数据损坏或者传输失败,这不会是我们想要看到的,所以Linux使用了D状态,他表示进程进入了深度睡眠,无法被操作系统通过常规手段杀掉。
这里能力不足,无法演示qwq…
4).T(stopped)
stopped 状态表示进程被信号暂停了执行,此时进程不占用 CPU 资源,也不会继续执行任何指令。
下面是kill指令,用来强制暂停进程
通过kill -19 2433 暂停进程
此时如果我们使用kill -18 2433 continue进程就会导致进程从前台程序变为后台程序,且无法被 Ctrl + C 杀掉,只能通过 kill -9 杀掉进程。
5).t(tracing stopped)
Tracing stop 状态是 Linux 中 stopped 状态的一个特例。它表示进程被父进程(通常是调试器或跟踪工具)通过 ptrace 系统调用暂停,用于调试或分析进程的行为。
如我们使用gdb,就会触发可追踪的暂停。以便调试
t状态如下:
6).Z(僵尸进程)
进程可以创建子进程,那如果子进程被杀掉了,会发生什么呢?
系统并不会回收子进程的资源,因为子进程是由其父进程管理的,父进程如果一直不收回的话,该子进程就变为了僵尸进程。不会被收回!!!,一直占用空间。
7).孤儿进程
如果父进程被杀掉了呢,此时子进程又将何去何从。
父进程死亡后,子进程便会被PID=1的init进程领养,便于管理和避免出现僵尸进程。
四.进程优先级
CPU资源分配的优先顺序,便是进程的优先级。
1.查看进程优先级
linux下进程的优先级由 PRI(priority)和NI(nice) 共同表示。
PRI是实际优先级
NI是优先级的偏移量 (-20~19)
值越小优先级越高
PRI=基础优先级+NI
为什么Linux下设定PRI初始为80呢?
实际上在Linux下,PRI的值映射范围是0 ~ 139,其中(0 ~ 99为实时优先级,100 ~ 139为普通优先级)而设定初始的 PRI值和NI值 的目的就是为了使操作系统能够更好的管理进程的优先级,80为平衡值使有些系统级的进程能更优的分配优先级。
2.修改进程优先级
top
指令可以查看和修改优先级
sudo top -> r ->输入PID ->输入NI值
即可修改,结果如下
五.环境变量
环境变量 是操作系统中用于存储配置信息的一种变量。
环境变量就好比是操作系统的全局变量,它们为操作系统和应用程序提供重要的配置信息。
如:我们在编译C++/C语言程序的时候,编译器是如何知道动静态库的位置的呢?通过操作系统的环境变量即可,锁定动静态库的路径。
1.相关指令
1.使用echo指令以查询环境变量
echo $PATH
echo $USER
echo $HOME
......
2.使用env指令查询全部环境变量
env
3.export指令添加环境变量
export MY_VAR="Hello, World!"
2.基本的环境变量的理解
1.PATH
为什么有些方法可以直接使用,而 可执行程序 要带上路径才能使用呢
因为PATH环境变量,当我们直接输入指令的时候,shell会在PATH路径下搜索相应的可执行程序。
测试:
效果如下:(可以不用带路径了)重启电脑环境变量会自动恢复为初始状态
2.HOME
cd ~指令为什么可以直接使用?他是如何知道我的位置的?
cd ~指令直接访问了HOME,知晓家目录路径。
3.USER
有些指令我们无法通过普通用户去使用访问,这是因为他会检测USER是否为“root”。
但是通过sudo指令,可以短暂提权。他并不会修改USER的值。
。。。。。。
3.三个命令行参数
int main(int argc,char* argv[],char* envp[])
{
return 0;
}
以上的3个命令行参数应该并不少见
1).int argc
表示命令行参数的个数(argument count)
以ls指令为例
ls -a -l
我们知道linux是由C语言编写的,所以ls其实也是个C语言可执行程序
后面紧跟 -a -l 作为命令行参数
ls -a -l 的个数为2,故argc==2
2).char *argv[ ]
表示命令行参数的值(argument vector)
同理,argv这个指针数组存放的就是“-a” “-l”的指令,我们可以通过前两个参数来为我们的C/C++程序制定功能。
3).char *envp[ ]
表示环境变量(environment pointer)
char*envp[ ]会将环境变量传入,我们可以通过环境变量进行不同功能的划分。
envp可以不传输,因为每一个程序都会获得一张环境表,char * environ,可以直接通过访问该表来读取环境变量的值。
3.举例测试
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char* argv[])
{
if(argc!=0)
{
int x;
for(x=0;x<argc;x++)
{
if(strcmp(argv[x],"-a")==0)
{
printf("执行-a指令\n查询用户类型\n");
char*who=NULL;
who=getenv("USER");//可以直接通过getenv来获取
if(strcmp(who,"root")==0)
{
printf("我是root\n");
}
else{
printf("我是用户\n");
}
return 0;
}
}
}
printf("无法分析指令\n");
return 0;
}
六.进程地址空间
对于一个进程而言,进程地址空间实际上是一个虚拟内存空间,其中在Linux下操作系统通过mm_struct对进程的地址空间的每个区域进行划分,如:代码段,已初始化数据区域,未初始化数据区域,栈区,堆区…。
虚拟地址通过页表与物理地址形成映射。这样在运行进程的时候,CPU可以直接访问物理地址,虚拟地址可以对进程起到约束的作用,防止其访问其它物理内存。并且可以使不连续的物理内存变得逻辑连续。并且保证了进程独立性。
但是程序在编译的时候就已经有地址出现了,这是为何呢?
虚拟内存的规则不仅仅操作系统遵守,编译器也会遵守。当编译一个程序时,程序内部的函数地址,变量地址…都会有一个虚拟地址,当运行程序时,操作系统直接把该进程的代码放入代码段区域。CPU读取代码段的虚拟地址(函数地址,变量地址…),并直接通过页表对物理内存进行映射,CPU是无法看到物理地址的,他永远通过虚拟地址访问。
上述结构大致如下:
这里还有一个代码帮助理解。
eg:
int main()
{
int global = 100;
pid_t id=fork();
if(id==0)
{
while(1)
{
printf("我是子 :global=%d,&global=%p\n",global,&global);
sleep(1);
}
}
else{
int cnt=0;
while(1)
{
printf("我是爹 :global=%d,&global=%p\n",global,&global);
sleep(1);
cnt++;
if(cnt==3)
{
global=900;
printf("我是爹,global已被更改%d\n",global);
}
}
}
return 0;
}
为什么两个进程的全局变量地址相同但是全局变量的值不同呢,Linux下有一种功能叫做写时拷贝,当父进程新建子进程的时候,子进程会继承父进程的代码,并且还在同一个物理地址上,但是如果父进程或者子进程如果对其中的某一内存页进行修改时,就会触发写时拷贝,系统会新开一块区域交由子进程。
创作不易,恳请留赞,互三互三