文章目录
- 前言
- 1、冯诺依曼体系结构
- 2、操作系统
- 2.1、系统调用
- 3、进程
- 3.1、进程概念
- 3.2、进程描述—PCB
- 3.3、查看进程信息
- 3.4、通过系统调用获取进程标识符
- 3.5、通过系统调用创建子进程—fork()
- 4、进程状态
- 5、僵尸进程
- 6、孤儿进程
- 7、进程优先级
- 7.1、PRI和NI是什么?
- 7.2、查看进程优先级的命令,更改NI值
- 8、竞争性、独立性、并发、并行名词概念?
- 9、环境变量
- 9.1、环境变量是什么?
- 9.2、Liunx中常见的环境变量
- 9.3、查看Liunx中的环境变量的方法
- 9.4、和环境变量相关的指令
- 9.5、环境变量存储在哪里?如何获取环境变量
- 10、环境变量具有全局性
- 11、程序地址空间——进程地址空间
- 11.1、虚拟地址空间
- 12、为什么要有虚拟地址空间??
前言
在进程这一章节主要讲述,进程的相关概念、进程的状态、如何创建进程以及僵尸/孤儿进程等……
1、冯诺依曼体系结构
图中即简要描述一台计算机主要的硬件资源:
每个人用的计算机,即个人笔记本等,都是由一个一个硬件组成的:
输入设备:键盘、鼠标、麦克风等读入用户写入的各种数据的设备。
中央处理器(CPU):主要由存储器和运算器组成。
输出设备:显示器、打印机等。
对于结构图中的存储器,此处主要指的是内存。所有需要和cpu打交道的都必须先加载到内存。
2、操作系统
操作系统,是每个人耳熟的一个词语,那么它是什么呢?其实操作系统是属于软件部分的,是连接底层硬件资源和上层用户的桥梁。在操作系统中,做好了对文件管理、进程管理、内存管理、硬件的驱动管理,对用户就提供对应管理的系统接口,通过接口访问系统管理的资源。
2.1、系统调用
3、进程
3.1、进程概念
使用windows系统的时候,可以通过
ctrl+Alt+Delete
快捷键查看任务管理器的,在其中就可以看到一个进程名称:
当然进程可以分为前台进程和后台进程:
前台进程
:前台进程是指当前正在与用户交互的进程,通常负责处理用户输入和显示输出结果。
后台进程
:后台进程是指在后台运行的进程,不直接与用户交互。它们通常为前台进程或其他系统组件提供支持和服务。
因此:
从书面概念来看:进程是指程序的一个执行实例、正在执行的程序等。
从内核观点:担当分配系统资源的实体。
3.2、进程描述—PCB
进程=PCB(内核数据结构)+可执行程序自己的代码和数据
。
可执行程序。在没点击该执行程序的时候,那么此执行程序的代码和数据都是在磁盘中存储的;然而,当我你们双击可执行程序后,首先就会加载到内存,因为要让CPU访问到此执行程序的代码和数据,必须先加载到内存(冯诺依曼体系结构决定的)。需要明确的是,电脑在开机到进入主界面的过程,实则是在把操作系统加载到内存的,也就是说我们在加载可执行程序之前,内存里面已经有了操作系统了,那么此时操作系统就要对加载进来的可执行程序做管理,如何管理?
利用一个struct结构体进行管理,里面是存储可执行程序的所有属性,每加载进来一个可执行程序,os就会用struct结构体创建一个结构体对象,记录该程序的所有属性,会将多个结构体对象用next指针链接起来,形成链表结构,整个就形成一个列表,叫做进程列表,之后对加载进来的程序做管理,就转换为对该链表做增删查改的操作了。那么该结构体的整个结构是内核数据结构,叫做PCB(进程控制块)。
在Linux
的内核中PCB的名称是:Lniux内核代码中是用双链表来进行管理的。
struct task_struct {
具体的所有属性
}
那么,进程的所有属性,都可以直接或间接的通过task_struct找到。
属性包括很多:
•标⽰符
: 描述本进程的唯⼀标⽰符,⽤来区别其他进程。
•状态
: 任务状态,退出代码,退出信号等。
•优先级
: 相对于其他进程的优先级。
•程序计数器
: 程序中即将被执⾏的下⼀条指令的地址。
•内存指针
: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
•上下⽂数据
: 进程执⾏时处理器的寄存器中的数据[休学例⼦,要加图CPU,寄存器]。
•I∕O状态信息
: 包括显⽰的I/O请求,分配给进程的I∕O设备和被进程使⽤的⽂件列表。
等……
下面是Liunx内核0.11的PCB结构中的各中属性:
3.3、查看进程信息
- 在Liunx中,所有的进程信息存储在
/proc
目录下的:
- 也可以通过指令获取自己运行起来的进程:
ps ajx | head -1 ; ps ajx grep 进程名
,下面查看的是自己运行的ocde进程:
ppid
:表示父进程的标识符id
pid
:即当前进程的标识符id
3.4、通过系统调用获取进程标识符
进程id:(
PID
)
父进程id:(PPID
)
1 #include<stdio.h>2 #include <sys/types.h>3 #include<unistd.h>4 5 int main()6 {7 while(1)8 {9 printf("父进程ppid:%d\n",getppid());10 printf("当前进程pid:%d\n",getpid());11 sleep(1);12 }13 return 0;14 }
3.5、通过系统调用创建子进程—fork()
- 下面是通过在
XShell
使用man fork
查看的:
- 先通过下面代码看现象:在父进程中创建子进程,各自打印自己的pid:
code.c ?? buffers 1 #include<stdio.h>2 #include <sys/types.h>3 #include<unistd.h>4 #include<stdlib.h> 5 6 int main()7 {8 //调用系统调用创建子进程9 pid_t id=fork();10 if(id<0)11 {12 perror("fork error");13 exit(2);14 }15 else if(id==0)16 {17 //child_process18 while(1)19 {20 printf("I am child process, my pid: %d, my parent pid: %d\n",getpid(),getppid());21 sleep(1);22 }23 }24 else25 {26 //parent_process27 while(1)28 {29 printf("I am parent process, my pid: %d, my parent pid: %d\n",getpid(),getppid());30 sleep(1);31 }32 }33 return 0;34 }
通过./code
运行该进程后,可以通过ctrl+c
终止,结果如图:
4、进程状态
在Liunx中描述一个进程的状态是通过整型值描述,只是宏定义过,在内核中所有状态是以task_state_array[]
管理起来的:
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 */
};
在进程的task_struct中有一个变量,即整型的,来表示当前进程的运行状态,下面是所有状态的解释:
在课本操作系统理论中可以看到运行、阻塞、挂起、就绪、结束等状态。而具体到Linux操作系统的时候,可以有
R(running)
:运行状态,在进程需频繁进行IO操作的,多数时间看到是S状态,也有R状态。
S(sleeping)
:浅睡眠,是可中断休眠。
D(disk sleep)
:磁盘休眠,即深度睡眠,是不可中断休眠,比如在进程给磁盘写入数据量比较大的时候,需要等待磁盘写完后返回给进程是否写入成功,进程再告诉给用户。在写入时候进程需要长时间等待,此时操作系统是不可以捎杀掉该休眠进程的,此进程是D状态。
T(stopped)
:暂停状态,
t(tracing stop)
:暂停状态,比如在进行gdb调式进程打断点后,运行该进程就会在断点处暂停。
X(dead)
:死亡状态,即结束状态
Z(zombie)
:僵尸状态,该状态就是为了获取该进程退出的信息,一般在子进程X后,需要把相关信息返回给父进程,让父进程知道后子进程就结束了,在返回给父进程相关信息的时候,子进程就处于僵尸状态。
注意:若子进程结束后,父进程一直不管,不会回收子进程的推出信息,那么子进程Z状态会一直存在,即内存泄漏问题。
下面可以通过while :; do ps axj | head -1 && ps axj | grep code | grep -v grep ; echo "****************************************"; sleep 1 ; done
部署一下,实时监控自己运行进程的实时状态:
5、僵尸进程
僵尸进程状态产生的情况是:当子进程运行结束退出后,父进程还在运行,并且父进程没有对子进程进行等待,此时子进程会一直在进程列表中,一直在等待父进程回收,此时就是处于僵尸状态,标识符是Z
下面通过代码演示僵尸进程的情况:
int main()11 {12 13 pid_t pid=fork();14 if(pid<0)15 {16 perror("fork error");17 exit(2);18 }19 else if(pid==0)20 {21 //child22 printf("我是子进程,pid:%d, 我的父进程是:%d\n",getpid(),getppid());23 sleep(10);//10秒后退出24 }25 else26 {27 //parent一直运行打印28 while(1)29 {30 printf("我是父进程,pid:%d\n",getpid());31 sleep(1);32 } 33 }34 return 0;35 }36
僵尸进程一直不回收,会对资源造成浪费和内存泄漏;浪费是因为,子进程是对父进程所的拷贝,有自己的pcb,若父进程一直不回收子进程,那么子进程即使运行结束,由于父进程没等待,不知道子进程运行的情况,此时子进程就一直处于僵尸状态,它的pcb会一直被维护,没有释放,因此造成资源的浪费。由于没有回收,那么造成内存泄漏也是存在的。
说到父进程不等待的话,就一直是僵尸状态,因此后面会讲述到如何处理僵尸进程。
6、孤儿进程
孤儿进程,即父进程先退,子进程就被称为’‘孤儿进程’,我们知道子进程退出需要父进程等待回收,不然子进程一直是僵尸状态,那么此时子进程已经是孤儿进程了,在子进程退出后该如何处理呢?
这时操作系统就会让1
号进程init领养孤儿进程:
下面通过代码演示孤儿进程的变化:父进程执行打印后,等待3秒退出,子进程等待10秒退出。
1
号进程即bash
进程。所有进程的父进程。
7、进程优先级
进程优先级是什么?即进程得到CPU资源的先后顺序。
那么为什么要有进程优先级呢?主要是CPU资源稀缺。
进程优先级是如何表示?通过数字表示的,数字的值越大,优先级就越低,反之数字的值越小,优先级越高。那么数字的值范围是多少呢?
我们先来看一看一个进程所描述的信息:
7.1、PRI和NI是什么?
首先,进程最终的优先级是由PRI
和NI
一起决定的,即PRI(new)=PRI(old)+NI
,最终PRI(new)
值越小优先级就越高。
那么NI
表示进程可别执行的优先级的修正数值,称为"nice值
"。通过公式可以看到,首先PRI
值是确定的,为80
,那么最终优先级由NI
确定,NI
为负数的时候,PRI
越小,则优先级就越高,然而NI
是有范围的:[-20,19],一共40个级别。
7.2、查看进程优先级的命令,更改NI值
使用top
命令更改已经存在进程的nice
值,从而看到优先级PRI
的变化。操作系统的禁止频繁修改NI值的。
由于NI的范围是[-20,19],而PRI=80,因此最终的进程优先级范围就是[60,99]。
8、竞争性、独立性、并发、并行名词概念?
竞争性:主要由于系统中进程数目很多,而CPU资源的非常稀缺的,每个进程都想快点被CPU调度,因此各个进程之间就有竞争属性,为了高效运行,就有了优先级,确保CPU资源合理竞争。
独立性:即各个进程都有自己的资源空间,是互不干扰的。
并发:多个进程在一个CPU下,采用进程切换的方式,在一段时间内,让每个进程都可以运行一点,一起推进。进程切换是非常快的,所以看不出每个进程运行的差距,然用户感觉每个进程一起在运行。
并行:即多个进程在多个CPU中,分别同时运行。
9、环境变量
9.1、环境变量是什么?
使用windows的时候,相信都配置过系统变量的,也就是在菜单搜索环境变量
里面就有用户环境变量个系统环境变量。然而环境变量,一般是用来指定操作系统运行环境的一些参数。比如在使用pyhton
的时候,需要下载解释器,并把解释器中bin
目录的路径添加到用户或者系统环境变量中的path
中,之后在运行python代码,可以找到解释器相关的功能模块。
在系统中的环境变量具有全局性。
9.2、Liunx中常见的环境变量
PATH:指定命令的搜索路径。
HOME:指定用户的主工作目录,家目录。
SHELL:即当前的Shell,通产是/bin/bash
9.3、查看Liunx中的环境变量的方法
echo $name
:name是环境变量名称,比如下面:
在PATH环境变量中的指令是可以直接不带路径运行的,下面用自己写的程序指令演示一下,就可以看到PATH环境变量的作用:
在不加入PATH之前,运行不能像自带指令那样直接输入名称就可以运行,此时是需要带路径运行的,下面通过
export PATH=$PATH:可执行程序所在的路径
加入到PATH中:
9.4、和环境变量相关的指令
echo
:显示某个环境变量的值,echo $name
export
:设置一个新的环境变量
env
:显示所有的环境变量
unset
:清楚环境变量
set
:显示本地定义的shell变量和环境变量
9.5、环境变量存储在哪里?如何获取环境变量
在Liunx中,有一张叫环境便变量表的东西,本质就是一个字符指针数组,里面存储的就是每个环境变量:
环境变量表也是具有全局性的,每个进程都会收到一张环境变量表,我们看到该表是一个字符指针数组,因此可以通过一下方式获取:
- 通过命令行第三个参数获取
这里参数,就不得不提到我们写程序的时候,main函数有参数?我们一般是写的不带参的,即int main()
,一般没用到环境变量,所以没写,其实原本的参数是:int main(int argc ,char* argv[],char* env[])
,可以看到第三个参数就是环境变量表,这里就可以通过它获取:
9 int main(int argc,char* argv[],char* env[])10 {11 int i=0;12 for(i=0;env[i];i++)//因为环境变量表末尾是NULL结尾的 13 {14 printf("%s\n",env[i]);15 }16 return 0;17 }
- 通过第三方变量获取
这里的第三方变量是environ
,此变量是libc
库中定义的全局变量,指向的环境变量表,由于它没有在任何一个头文件中,因此在使用的时候需要先用extern
声明:
8 int main()//由于没用到环境变量表,因此可以省略不写参数9 {10 extern char** environ;11 int i=0;12 for(i=0;environ[i];i++)13 {14 printf("%s\n",environ[i]); 15 }16 return 0;17 }
- 通过系统调用获取
系统函数接口:getenv(“环境变量名”)
,一般的获取单个的环境变量值,特定需要查看的环境变量;
10 int main()11 {12 printf("%s\n",getenv("PWD"));13 printf("%s\n",getenv("PATH"));14 printf("%s\n",getenv("HOME")); 15 return 0;16 }
10、环境变量具有全局性
这里全局性,通过创建子进程来观察,在进程中,先打印环境变量看一看,然后再导入一个新的环境变量,在子进程中打印看一看,是否有添加的新的环境变量:
7 int main()8 {9 //先获取一个不存在,运行会报错,无法找到10 printf("%s\n",getenv("MYNAME")); 11 extern char** environ;12 //子进程中查看13 if(fork()==0)14 {15 int i=0;16 for(i=0;environ[i];i++)17 {18 printf("%s\n",environ[i]);19 }20 }21 return 0;22 }
通过用命令行输入export MYNAME="你好"
导入进去后,返现子进程中也查看到了,说明环境变量是全局性的,可以被后面的子进程继承。
11、程序地址空间——进程地址空间
通过一张图了解一下32位平台下的空间布局:
下面通过一段代码,看一下空间分布:
7 //定义全局变量和全局静态变量8 int g_num=100;9 static int g_val=20;10 int main(int argc, char *argv[], char *env[])11 {12 const char *str = "helloworld";13 printf("code addr: %p\n", main);14 printf("init global addr: %p\n", &g_num);//全局变量在静态区的,因为静态区存储静态变量和全局变量15 printf("uninit global addr: %p\n", &g_val);16 static int test = 10;17 char *heap_mem = (char*)malloc(10);18 char *heap_mem1 = (char*)malloc(10); 19 char *heap_mem2 = (char*)malloc(10);20 char *heap_mem3 = (char*)malloc(10);21 printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)22 printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)23 printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)24 printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)25 26 printf("test static addr: %p\n", &test); //heap_mem(0), &heap_mem(1)27 printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)28 printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)29 printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)30 printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)31 printf("read only string addr: %p\n", str);32 for(int i = 0 ;i < argc; i++)33 {34 printf("argv[%d]: %p\n", i, argv[i]);35 }36 for(int i = 0; env[i]; i++)37 {38 printf("env[%d]: %p\n", i, env[i]);39 }40 return 0;41 }
从代码运行后,可以看出不同变量存储的位置差异。然而,我们能打印出看到是内存地址吗?其实不然,这些叫做虚拟地址。
11.1、虚拟地址空间
例子:有一个已初始化的全局变量,在子进程中进行修改,并打印该全局变量和地址,父进程只显示该全局变量值和地址。
现象就是子进程中该变量是一直在改变的,而父进程是一开始初始化的值,但是两个进程中该全局变量的地址是一样的,因此说明不是内存地址,而是虚拟地址。同一变量地址一样,但是内容不一样主要用到是写时拷贝。
my_code.c ?? buffers 1 #include<stdio.h>2 #include <sys/types.h>3 #include<unistd.h>4 #include<stdlib.h>5 6 //已经初始化的全局变量7 int g_val=100;8 int main()9 {10 //创建子进程11 pid_t pid=fork();12 if(pid<0)13 {14 perror("fork error");15 exit(2);16 }17 else if(pid==0)18 {19 //child20 while(1)21 { 22 printf("我是子进程,pid=%d,我的父进程pid=%d, 全局变量g_val=%d,&g_val=%p\n",getpid(),getppid(),g_val++,&g_val);23 sleep(1);24 }25 }26 else27 {28 //parent29 while(1)30 {31 printf("我是父进程,pid=%d,我的父进程pid=%d, 全局变量g_val=%d,&g_val=%p\n",getpid(),getppid(),g_val,&g_val);32 sleep(1);33 }34 }35 return 0;36 }
~
~
看到的现象就是上面描述一样的,这是为什么呢?通过下图可以说明:
在调用fork()
函之后,创建子进程会拷贝父进程的task_struct、程序地址空间、页表、内存布局,即和父进程一样的,若子进程中不对公共资源的变量数据作更改时,那么数据和地址一样,但是此处在子进程中对公共的全局变量作更改,此时在内存空间中次采用写时拷贝的方式,开一个和g_val一样大的内存空间,同时在子进程的页表中更新旧的物理地址,此时子进程页表中就是新的映射关系,在内存中两个进程的g_val就有独自的空间,这就保持了进程的独立性,两个进程的页表中的连接着程序地址空间的一部分是一样的,物理地址指向不一样,所以现象就是运行的那样。
现象本质:①一个进程就要有一个task_struct,在该结构体中有一个虚拟地址空间,即是定义一个结构体mm_struct,它是在task_struct结构体中,在此结构体中是定义的每个区域的起始地址和结束地址变量来存储对应区域的起始地址和结束地址。②一个进程就要有一个页表。③页表是用来做虚拟地址和物理地址映射的。
最后得出这些地址是虚拟地址。
12、为什么要有虚拟地址空间??
回答该问题可以反过来看,要是没有虚拟地址空间会有什么影响?
在没有虚拟地址空间的情况下,每个进程直接使用物理内存地址,这会导致不同进程访问同一块物理地址时发生冲突。