Linux教程

Linux进程概念

本文主要是介绍Linux进程概念,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • 基本概念
  • 描述进程-PCB
    • task_struct-PCB的一种
    • task_ struct内容分类
  • 组织进程
  • 查看进程
    • 通过系统目录查看
    • 通过ps命令查看
  • 通过系统调用获取进程的PID和PPID
  • 通过系统调用创建进程- fork初始
    • fork函数创建子进程
    • 用 if 进行分流
  • Linux进程状态
    • 运行状态-R
    • 浅度睡眠状态-S
    • 深度睡眠状态-D
    • 停止状态-T
    • 僵尸状态-Z
    • 死亡状态-X
  • 僵尸进程
  • 僵尸进程的危害
  • 孤儿进程
  • 进程优先级
    • 基本概念
    • 查看系统进程
    • PRI与NI
    • 通过top命令更改进程的nice值
    • 通过renice命令更改进程的nice值
    • 四个重要概念
  • 环境变量
    • 基本概念
    • 常见环境变量
    • 查看环境变量的方法
    • 测试PATH
    • 测试HOME
    • 和环境变量相关的命令
    • 环境变量的组织方式
    • 通过代码如何获取环境变量
    • 通过系统调用获取环境变量
  • 程序地址空间
  • 进程地址空间

基本概念

课本概念: 程序的一个执行实例,正在执行的程序等
内核观点: 担当分配系统资源(CPU时间,内存)的实体。

当我们打开可执行程序时,本质上就是把该程序加载到内存中。只有将其加载到内存中,CPU才能进行逐行的语句执行。当程序加载到内存时,我们可以称之为进程。

在这里插入图片描述

描述进程-PCB

  • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合
  • 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct

我们可以用ps aux查看当前系统中存在的进程
在这里插入图片描述

task_struct-PCB的一种

Linux进程控制模块是用结构体实现的

  • 在Linux中描述进程的结构体叫做task_struct。
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息

task_ struct内容分类

task_struct当中主要包含以下信息

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据: 进程执行时处理器的寄存器中的数据
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息

组织进程

操作系统是如何对进程进行管理的?

这里要牢记六字真言:先描述,再组织。 当进程出现时,操作系统立刻对其进行描述,再对描述的内容进行管理,将其储存在进程控制模块的数据结构中,俗称PCB(process control block)。

操作系统将每一个进程都进行描述,形成了一个个的进程控制块(PCB),并将这些PCB以双链表的形式组织起来。
在这里插入图片描述

查看进程

通过系统目录查看

进程的信息可以通过 /proc 系统文件夹查看
在这里插入图片描述如上图系统文件中有很多数字,这些数字就是进程的PID,对应的文件中记录着该进程的各种信息。若想查看进程的信息打开该文件夹即可。

在这里插入图片描述

通过ps命令查看

单独使用ps命令,会显示所有进程信息。

指令: ps aux

在这里插入图片描述ps命令与grep命令搭配使用,即可只显示某一进程的信息。

指令: ps aux | head -1 && ps aux | grep myproc(进程) | grep -v grep

在这里插入图片描述

通过系统调用获取进程的PID和PPID

  • 进程id(PID)
  • 父进程id(PPID)

我们可以通过getpid和getppid即可分别获取进程的PID和PPID

在这里插入图片描述当上述代码运行时,可循环打印PID,PPID
在这里插入图片描述
我们可以通过ps指令查看更全面的信息

在这里插入图片描述

通过系统调用创建进程- fork初始

fork函数创建子进程

观察下面代码
在这里插入图片描述
运行结果

在这里插入图片描述
这里使用fork函数创建子进程,父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)

用 if 进行分流

若是想让父子进程执行不同的功能,上文程序达不到要求,这里就要用到if分流

fork函数的返回值:
1、如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0。
2、如果子进程创建失败,则在父进程中返回 -1。

通过fork返回值的不同,可以让父子进程执行不同的功能

在这里插入图片描述此时父子进程可执行不同的功能

在这里插入图片描述

Linux进程状态

在这里插入图片描述
Linux操作系统的源代码当中对于进程状态有如下定义:

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char *task_state_array[] = {
	"R (running)",       /*  0*/
    "S (sleeping)",      /*  1*/
    "D (disk sleep)",    /*  2*/
    "T (stopped)",       /*  4*/
    "T (tracing stop)",  /*  8*/
    "Z (zombie)",        /* 16*/
    "X (dead)"           /* 32*/
};

在Linux操作系统当中我们可以通过 ps aux 或 ps axj 命令查看进程的状态。
在这里插入图片描述

在这里插入图片描述

运行状态-R

一个进程处于运行状态(running),并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。

所有处于运行状态,即可被调度的进程,都被放到运行队列当中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行。

浅度睡眠状态-S

一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep))。

执行如下代码

在这里插入图片描述
在进程休眠的100秒钟我们可以看到进程处于浅度睡眠状态

在这里插入图片描述而处于浅度睡眠状态的进程是可以被杀掉的,我们可以使用kill命令将该进程杀掉。

在这里插入图片描述

深度睡眠状态-D

一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。

例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(磁盘休眠状态)

停止状态-T

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


在这里插入图片描述在这里插入图片描述
注意:

可以通过kill -l指令查看当前系统所支持的信号集。
在这里插入图片描述

僵尸状态-Z

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

死亡状态-X

死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以在任务列表当中不易看到死亡状态(dead)。

僵尸进程

我们来模拟一个僵尸进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	printf("I am running...\n");
	pid_t id = fork();
	if(id == 0){ //child
		int count = 5;
		while(count){
			printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
			sleep(1);
			count--;
		}
		printf("child quit...\n");
		exit(1);
	}
	else if(id > 0){ //father
		while(1){
			printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
	}
	else{ //fork error
	}
	return 0;
} 

我们可以通过循环指令不断查看进程状态来观察僵尸进程

Linux下循环执行某个指令方式

while :; do (指令内容); sleep 1; done

在这里插入图片描述可以看出子进程退出后就变成了僵尸进程

僵尸进程的危害

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于僵尸状态。
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
  • 若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占用内存。
  • 僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,意味着,僵尸进程会导致内存泄漏。

孤儿进程

父进程先退出,子进程就称之为“孤儿进程”,孤儿进程被1号init进程领养,由init进程回收。

模拟一个孤儿进程

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 
  5 int main()
  6 {
  7         printf("I am running...\n");
  8         pid_t id = fork();
  9         if(id == 0){ //child
 10                 while(1){
 11                         printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
 12                         sleep(1);
 13                 }
 14         }
 15         else if(id > 0){ //father
 16                 int count=5;
 17                 while(count){
 18                         printf("I am father...PID:%d, PPID:%d,count:%d\n", getpid(), getppid(),count);
 19                         sleep(1);
 20                         count--;
 21                 }
 22                 printf("father quit ... \n");
 23                 exit(1);
 24 
 25         }
 26         else{ //fork error
 27         }
 28         return 0;
 29 }

在这里插入图片描述

进程优先级

基本概念

  • cpu资源分配的先后顺序,就是指进程的优先权(priority)。
  • 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
  • 可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

查看系统进程

在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:

在这里插入图片描述
其中的几个重要信息如下:

  • UID : 代表执行者的身份
  • PID : 代表这个进程的代号
  • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI :代表这个进程可被执行的优先级,其值越小越早被执行
  • NI :代表这个进程的nice值

PRI与NI

  • PRI是进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
  • NI是nice值,表示进程可被执行的优先级的修正数值
  • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
  • nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
  • 调整进程优先级,在Linux下,就是调整进程nice值
  • nice其取值范围是-20至19,一共40个级别。

注意: 在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI。

通过top命令更改进程的nice值

top命令就相当于Windows操作系统中的任务管理器,它能够动态实时的显示系统当中进程的资源占用情况。

在这里插入图片描述使用top命令后按“r”键,会要求你输入待调整nice值的进程的PID。

在这里插入图片描述输入进程PID并回车后,会要求你输入调整后的nice值。
在这里插入图片描述输入nice值后按“q”即可退出

注意: 若是想将NI值调为负值,也就是将进程的优先级调高,需要使用sudo命令提升权限。

通过renice命令更改进程的nice值

使用renice命令,后面跟上更改后的nice值和进程的PID即可。

在这里插入图片描述
注意: 若是想使用renice命令将NI值调为负值,也需要使用sudo命令提升权限。

在这里插入图片描述

四个重要概念

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

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

并行: 多个进程在多个CPU下分别同时进行运行,这称之为并行。

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

环境变量

基本概念

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

常见环境变量

  • PATH : 指定命令的搜索路径
  • HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
  • SHELL : 当前Shell,它的值通常是/bin/bash。

查看环境变量的方法

我们可以通过echo命令来查看环境变量

echo $NAME //NAME为待查看的环境变量名称

例:
查看环境变量PATH
在这里插入图片描述

测试PATH

为什么我们自己生成的可执行程序必须要在前面带上./才可以执行,而系统的指令可以直接执行?

当想运行一条可执行程序时,系统必须知道该程序的路径。系统提供的指令存放在指定路径下,而我们自己创建的可执行程序存放在当前路径下,所以需要在前面带上./来提示系统。

系统提供的指令存放路径如下

在这里插入图片描述
可以看到环境变量PATH当中有多条路径,这些路径由冒号隔开,当你使用ls命令时,系统就会查看环境变量PATH,然后默认从左到右依次在各个路径当中进行查找。

我们也可以查看任意一条系统指令存放的路径

在这里插入图片描述

有没有社什么办法让自己编译的可执行程序可以直接运行呢?

下面有两种方法

方式一:将可执行程序拷贝到环境变量PATH的某一路径下。

在这里插入图片描述
方式二:将可执行程序所在的目录导入到环境变量PATH当中。

在这里插入图片描述

测试HOME

任何一个用户在运行系统登录时都有自己的主工作目录(家目录),环境变量HOME当中即保存的该用户的主工作目录。

在这里插入图片描述

和环境变量相关的命令

  1. echo: 显示某个环境变量值
  2. export: 设置一个新的环境变量
  3. env: 显示所有环境变量
  4. unset: 清除环境变量
  5. set: 显示本地定义的shell变量和环境变量

环境变量的组织方式

在这里插入图片描述

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。

通过代码如何获取环境变量

main是有参数的,只是平时我们用不到

main函数有三个参数

int main(int argc, char *argv[], char *env[])

我们先讲前两个参数

输入如下代码
在这里插入图片描述

运行结果如下
在这里插入图片描述
main函数的第二个参数是一个字符指针数组,数组当中的第一个字符指针存储的是可执行程序的位置,其余字符指针存储的是所给的若干选项,最后一个字符指针为空,而main函数的第一个参数代表的就是字符指针数组当中的有效元素个数。

在这里插入图片描述
通过这两个参数可以让一条指令添加不同的符号执行不同的功能

如 ls -a 列出目录下的所有文件,包括以 . 开头的隐含文件。
在这里插入图片描述
而 ls -l 是列出文件的详细信息。
在这里插入图片描述
下面模拟实现指令+不同符号执行不同功能

#include <stdio.h>                                                                                                                         
#include <string.h>
int main(int argc, char *argv[], char* envp[])
{
	if(argc > 1)
	{
		if(strcmp(argv[1], "-a") == 0)
		{
			 printf("you used -a option...\n");
		}
		else if(strcmp(argv[1], "-b") == 0)
		{
			printf("you used -b option...\n");
		}
		else
		{
			printf("you used unrecognizable option...\n");
		}
	}
	else
	{
		printf("you did not use any option...\n");
	}
	return 0;
}

在这里插入图片描述

main函数的第三个参数接收的是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量。


在这里插入图片描述
运行结果就是各个环境变量的值:

在这里插入图片描述
除了使用main函数的第三个参数来获取环境变量以外,我们还可以通过第三方变量environ来获取。

在这里插入图片描述在这里插入图片描述
注意: libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern进行声明。

通过系统调用获取环境变量

除了通过main函数的第三个参数和第三方变量environ来获取环境变量外,我们还可以通过系统调用getenv函数来获取环境变量。getenv函数可以根据所给环境变量名,在环境变量表当中进行搜索,并返回一个指向相应值的字符串指针。

例:用getenv函数获取环境变量PATH
在这里插入图片描述

运行结果如下

在这里插入图片描述

程序地址空间

程序地址空间分布图:
在这里插入图片描述

观察下面代码及运行结果

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
 	pid_t id = fork();
 	if(id < 0){
 		perror("fork");
 		return 0;
 	}
 	else if(id == 0){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
 		g_val=100;
 		printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
 	}else{ //parent
 		sleep(3);
 		printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
 	}
 	sleep(1);
 	return 0;
}

在这里插入图片描述

从运行结果可看出,父子进程在同一块地址上取的值竟然不相同,这时为什么呢?

实际上,我们在语言层面上打印出来的地址都不是物理地址,而是虚拟地址。物理地址用户是看不到的,是由操作系统统一进行管理的。

在这里插入图片描述

注意: 虚拟地址和物理地址之间的转化由操作系统完成。

进程地址空间

进程地址空间是内存中的一种内核数据结构,在Linux当中进程地址空间由结构体mm_struct实现。

进程地址空间将0x00000000到0xffffffff地址划分为各个区域,如代码区、堆区、栈区等。而结构体mm_struct记录了各个边界刻度。

在结构体mm_struct当中,各个边界刻度之间的每一个刻度都代表一个虚拟地址,这些虚拟地址通过页表映射与物理内存建立联系。由于虚拟地址是由0x00000000到0xffffffff线性增长的,所以虚拟地址又叫做线性地址。

每个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也会随之被创建。而操作系统可以通过进程的task_struct找到其mm_struct,因为task_struct当中有一个结构体指针存储的是mm_struct的地址。
例如,父进程有自己的task_struct和mm_struct,该父进程创建的子进程也有属于其自己的task_struct和mm_struct,父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置,如下图:

在这里插入图片描述
而当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。

当子进程的值修改时,物理内存需要开一块空间储存新值,并改变虚拟地址通过页表映射的物理地址。

在这里插入图片描述

下面回答几个问题

  1. 为什么数据要进行写时拷贝?

进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。

  1. 为什么不在创建子进程的时候就进行数据的拷贝?

子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间。

  1. 代码会不会进行写时拷贝?

正常情况下是不会的,只有再特殊情况下才会,例如在进行进程替换的时候,则需要进行代码的写时拷贝。

  1. 为什么要有进程地址空间?

(1)有了进程地址空间后,就不会有任何系统级别的越界问题存在了。例如进程1不会错误的访问到进程2的物理地址空间,因为你对某一地址空间进行操作之前需要先通过页表映射到物理内存,而页表只会映射属于你的物理内存。总的来说,虚拟地址和页表的配合使用,本质功能就是包含内存。

(2)有了进程地址空间后,每个进程都认为看得到都是相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序等都是相同的,这样一来我们在编写程序的时候就只需关注虚拟地址,而无需关注数据在物理内存当中实际的存储位置。

(3)有了进程地址空间后,每个进程都认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程调度与内存管理进行解耦或分离。

这篇关于Linux进程概念的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!