『Linux』进程概念

冯诺依曼体系结构

1946年美籍匈牙利科学家冯·诺依曼提出存储程序原理把程序本身当做数据来对待程序和该程序处理的数据用同样的方式存储,并确定了存储程序计算机的五大组成部分和基本工作方法
半个多世纪以来,计算机制造技术发生了巨大变化,但冯·诺依曼体系结构仍然沿用至今,人们将冯·诺依曼称为计算机界的祖师爷。
『Linux』进程概念_第1张图片
注意

  • 这里的存储器指的是内存
  • 不考虑缓存情况,这里的CPU只能对内存进行读写,不能访问外设(输入或输出设备)
  • 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取
  • 总而言之,所有设备都只能直接和内存打交道

如今,我们的计算机都是使用冯·诺依曼体系结构,包含以下组成部分:

  • 输入单元:包括键盘、鼠标等
  • 中央处理器(CPU):包括运算器和控制器等
  • 输出单元:显示器等

下面我们使用一个QQ聊天中数据的流动过程来理解冯·诺依曼体系结构
当我们使用QQ和好友聊天时键盘是输入设备,输入设备将信息送入内存,CPU从内存中取出该数据进行处理,处理完毕后放回内存,然后处理后的数据通过网卡发送出去,这里网卡就是输出设备
好友接收消息,网卡接收到数据将数据送入内存。这里网卡是输入设备CPU从内存中取出该数据进行处理,处理完毕后放回内存,然后处理后的数据通过显示器进行显示,这里显示器就是输出设备

操作系统

操作系统(Operating System,简称OS)管理计算机硬件与软件资源的计算机程序(一款纯正的“搞管理”的软件),同时也是计算机系统的内核与基石。操作系统需要处理如管理与配置内存、决定系统资源供需的优先次序、控制输入设备与输出设备、操作网络与管理文件系统等基本事务。操作系统也提供了一个让用户与系统交互的操作界面


笼统的理解,操作系统包含

  • 内核(进程管理,内存管理,文件管理,驱动管理)
  • 其他程序(例如函数库,shell程序等等)

设计操作系统的目的

  • 与硬件交互,管理所有的软硬件资源(先描述,再组织)
  • 为用户程序(应用程序)提供一个良好的执行环境

计算机如何管理硬件

  • 先使用struct结构体对硬件进行描述
  • 再使用链表或其他高效的数据结构进行组织

系统调用与库函数

  • 开发角度操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用
  • 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

我们知道操作系统管理硬件资源是先组织再描述,那么对于运行中的程序的管理呢
同样的,操作系统对运行中程序的管理,同样是先描述,再组织

进程

什么是进程?

简单来说,进程就是运行中的程序
课本上概念程序的一个执行示例,正在执行的程序等
内核观点担当分配系统资源(CPU时间,内存)的实体

进程的描述和组织

描述

  • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
  • 进程控制块PCB(Process Control Block)Linux操作系统下的PCB是:task_struct
  • task_struct
    在Linux中描述进程的结构体叫做task_struct
    task_struct是Linux内核的一种数据结构(就是一个结构体),它会被装载到RAM(内存)里,并且包含着进程的信息

组织

  • 操作系统使用双向链表将task_struct组织起来

PCB中描述信息有什么?

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

进程查看

我们先跑起来一个进程

#include 
#include 

int main(){

	while(1){
		// 休眠1s
		sleep(1);
	}

	return 0;
}

在这里插入图片描述
另开一个终端,我们使用ps命令来查看一下

ps -aux | grep pcb | grep -v grep

在这里插入图片描述
可以看到其进程标识符为11369
我们再使用ls命令来查看一下

ls /proc/11369

『Linux』进程概念_第2张图片

查看进程标识符

上述可以看到,使用ps命令可以查看进程标识符,除了ps命令外,还可以使用系统调用来查看进程标识符

pid_t getpid(void);
返回值:
	调用进程的进程标识符。

pid_t getppid(void);
返回值:
	调用进程的父进程的进程标识符。
#include 
#include 

int main(){
	while(1){
		std::cout << "pid: " << getpid() << std::endl;
		std::cout << "ppid: " << getppid() << std::endl;

		// 休眠2s
		sleep(2);
	}

	return 0;
}

『Linux』进程概念_第3张图片
另开一个终端,使用ps命令来验证一下
在这里插入图片描述

进程状态

我们来看一下内核源码中怎么说

[sss@aliyun ~]$ vim /usr/src/kernels/3.10.0-957.5.1.el7.x86_64/include/linux/sched.h
/*
* Task state bitmask. NOTE! These bits are also
* encoded in fs/proc/array.c: get_task_state().
*
* We have two separate sets of flags: task->state
* is about runnability, while task->exit_state are
* about the task exiting. Confusing, but this way
* modifying one set can't modify the other one by
* mistake.
*/
#define TASK_RUNNING        0
#define TASK_INTERRUPTIBLE  1
#define TASK_UNINTERRUPTIBLE    2
#define __TASK_STOPPED      4
#define __TASK_TRACED       8
/* in tsk->exit_state */
#define EXIT_ZOMBIE     16
#define EXIT_DEAD       32
/* in tsk->state again */
#define TASK_DEAD       64
#define TASK_WAKEKILL       128
#define TASK_WAKING     256
#define TASK_PARKED     512
#define TASK_STATE_MAX      1024
  • R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里
  • S睡眠状态(sleeping): 进程在等待时间完成(这里的睡眠有时候也叫可中断睡眠(interrupltible sleep))
  • D磁盘休眠状态(Disk sleep): 有时候也叫作不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束
  • T停止状态(stopped): 可以通过发送SIGSTOP信号给进程来停止进程,这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行,停止状态的进程无法使用kill杀死,但是可以使用kill -9强杀
  • X死亡状态(dead): 这个状态只是一个返回状态,不会在任务列表里看到这个状态
  • 僵死状态(Zombies)是一个比较特殊的状态。当子进程退出并且父进程没有读取到子进程退出返回代码时,操作系统不会完全释放子进程的资源,子进程就称为僵尸进程,kill -9也杀不死

两种特殊的进程

僵尸进程

  • 僵死状态(Zombies)是一个比较特殊的状态。当子进程退出并且父进程没有读取到子进程退出返回代码时,操作系统不会完全释放子进程的资源,子进程就称为僵尸进程
  • 僵尸进程会以僵死状态保持在进程表中,直到父进程读取到子进程的退出返回代码
  • 所以只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进就如僵死状态

我们来创建一个僵死进程的示例

#include 
#include 
#include 

int main(){
	// 创建一个子进程
	pid_t pid = fork();

	if(pid < 0){
		perror("fork");
		return 1;
	} else if(pid > 0){ 
		// 父进程
		printf("parent[%d] is sleeping...\n", getpid());
		// 睡眠60s
		sleep(120);
	} else{
		// 子进程
		printf("child[%d] is begin Z...", getpid());
		// 睡眠5s
		sleep(60);
		return 0;
	}

	return 0;
}

『Linux』进程概念_第4张图片
僵尸进程的危害

  • 进程的退出状态必须被维持下去,因为它要告诉关心它的父进程,你交给我的任务,我办的怎么样了。如果父进程一直不读取,子进程就一直处于僵死状态
  • 退出状态属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,僵死状态一直不退出,就要一直维护PCB
  • 一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费

僵尸进程如何处理呢

  • 父进程退出即可

如何避免产生僵尸进程呢

  • 进程等待

孤儿进程

父进程先退出,子进程就称之为“孤儿进程”。孤儿进程被1号init进程领养
下面来看一段代码演示

#include 
#include 
#include 

int main(){
	// 进程创建
	pid_t pid = fork();

	if(pid < 0){
		perror("fork");

		return 1;
	} else if(pid == 0){
		// 子进程
		printf("Child Process: %d\n", getpid());
		// 进程休眠10s
		sleep(10);
	} else{
		// 父进程
		printf("Parent Process: %d\n", getpid());
		// 进程休眠3s
		sleep(3);

		return 0;
	}

	return 0;
}

『Linux』进程概念_第5张图片
一个终端运行上述代码

[sss@aliyun ~]$ while :; do ps -ef | grep orphan | grep -v grep;  sleep 1; echo "--------------------------------------------------------"; done

『Linux』进程概念_第6张图片
一个终端使用shell脚本观察

进程优先级

  • CPU资源分配的优先级,就是指进程的优先权(priority)
  • 优先权高的进程有优先执行权力。配置进程优先权对多任务环境的Linux很有用,可以改善系统性能,让操作系统运行的更加合理。
  • 可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能

其他概念

  • 竞争性系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的为了高效完成任务,更合理竞争相关资源,便有了优先级
  • 独立性多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行多个进程在多个CPU下分别,同时运行,称为并行
  • 并发多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称为并发

使用ps命令来查看系统进程
在这里插入图片描述
其中各个字段介绍如下

  • UID:代表执行者的身份
  • PID:代表这个进程的标识符
  • PPID:代表这个进程是由哪个进程创建的,即父进程的标识符
  • PRI:代表这个进程可被执行的优先级,其值越小越早被执行
  • NI:代表这个进程的nice值

PRI 和 NI

  • PRI比较好理解,就是进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小,进程的优先级别越高
  • NI就是我们常说的nice值了,其表示进程可被执行的优先级的修正数值
  • PRI值越小越快被执行,PRI的值不能直接修改,但可以通过修改nice值来间接的修改PRI,PRI = PRI + NI
  • nice值的取值范围:-20 ~ 19,一共40个级别

注意: 进程的nice值不是进程的优先级,它们不是一个概念,但是进程的nice值会影响到进程的优先级变化,可以将nice值理解为进程优先级的修正数据


可以使用top命令查看进程的优先级
『Linux』进程概念_第7张图片
使用top命令更改已经存在的进程的nice值

  • 输入命令top
    『Linux』进程概念_第8张图片
  • 按r键输入PID值
    『Linux』进程概念_第9张图片
  • 输入PID后回车,输入nice值,回车即可,这里就不演示了
    『Linux』进程概念_第10张图片

进程地址空间

内存空间分布图如下
『Linux』进程概念_第11张图片

代码演示

#include 
#include 
#include 

int main(){
	int val = 10;

	pid_t pid = fork();
	if(pid < 0){
		// 出错
		perror("fork error");
	}
	else if(pid == 0){
		// 子进程
		printf("child: address-> %p, value-> %d\n", &val, val);
	}
	else{
		// 父进程
		printf("parent: address-> %p, value-> %d\n", &val, val);
	}

	sleep(1);
	return 0;
}

『Linux』进程概念_第12张图片
可以发现输出的结果一模一样,很好理解,因为子进程是父进程的拷贝,所以数据应该是一样的,下面我们将代码改动一下

#include 
#include 
#include 

int main(){
	int val = 10;

	pid_t pid = fork();
	if(pid < 0){
		// 出错
		perror("fork error");
	}
	else if(pid == 0){
		// 子进程
		val = 20;
		printf("child: address-> %p, value-> %d\n", &val, val);
	}
	else{
		// 父进程
		printf("parent: address-> %p, value-> %d\n", &val, val);
	}

	sleep(1);
	return 0;
}

『Linux』进程概念_第13张图片
我们发现,父子进程,输出地址是一样的,但是变量的内容不一样。这是为什么呢?
首先变量内容不一样,所以父子进程输出的变量不是同一个变量。但是地址是一样的,这是因为我们看到的是虚拟地址。我们在C/C++语言中看到的地址,全都是虚拟地址,物理地址,用户一概看不到,由OS统一管理进程地址空间由一个结构体mm_struct来描述
『Linux』进程概念_第14张图片
由上图可以看出,同一个变量地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址上!
页表: 记录虚拟地址和物理地址之间的映射关系;并且对虚拟地址进行访问控制
程序地址空间优点: 内存充分利用,内存访问控制,保持进程独立性

环境变量

什么是环境变量

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

常见环境变量

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

查看环境变量的方法

# NAME:环境变量名称
[sss@aliyun ~]$ echo $NAME

在这里插入图片描述


下面我们来看一下环境变量的用处
首先来看一下PATH环境变量
首先我们来写一个程序

#include 

int main(){
	std::cout << "hello, world!" << std::endl;
	
	return 0;
}

然后生成可执行程序
『Linux』进程概念_第15张图片
然后执行./hello和hello对比二者的区别
『Linux』进程概念_第16张图片
我们可以看到,./hello可以执行,hello直接执行会出现命令找不到,为什么Linux下的命令可以直接执行,而我们生成的二进制程序不能直接执行,必须要加上路径才可以执行,这是为什么呢?
下面我们将我们生成的程序所在路径添加到PATH中再来直接运行hello

[sss@aliyun pcb]$ export PATH=$PATH:/home/sss/prictice/linux/pcb

『Linux』进程概念_第17张图片
从上面可以看出,将生成的hello程序所在路径添加到PATH环境变量中后,就可以直接运行hello程序,而不用带路径了,这说明,那些直接运行不需要带路径的命令,都是通过PATH环境变量实现的。


下面我们再来看一下HOME环境变量
首先我们切换到root用户,查看一下HOME环境变量

[sss@aliyun pcb]$ su
Password: 
[root@aliyun pcb]# echo $HOME

『Linux』进程概念_第18张图片
再切回sss用户,看一下HOME环境变量

[root@aliyun pcb]# exit
exit
[sss@aliyun pcb]$ echo $HOME

『Linux』进程概念_第19张图片


和环境变量相关的命令

  • echo:显示某个环境变量的值。
    在这里插入图片描述
  • export:设置一个新的环境变量。
    在这里插入图片描述
  • env:显示所有环境变量。
    『Linux』进程概念_第20张图片
  • set:显示本地定义的shell变量和环境变量。
    『Linux』进程概念_第21张图片
  • unset:清除环境变量。
    『Linux』进程概念_第22张图片

环境变量的组织方式
『Linux』进程概念_第23张图片
每个程序都会收到一张环境表环境表中是一个字符指针数组每个指针指向一个以’\0’结尾的环境字符串


通过代码获取环境变量

  • 方法一命令行第三个参数
#include 

int main(int argc, char* argv[], char* env[]){
	// 循环打印所有环境变量
	for(int i = 0; env[i] != nullptr; ++i){
		std::cout << env[i] << std::endl;
	}

	return 0;
}

『Linux』进程概念_第24张图片

  • 方法二通过第三方变量environ变量获取
#include 

int main(){
	// 第三方环境变量引入
	extern char** environ;

	// 循环打印所有环境变量
	for(int i = 0; environ[i]; ++i){
		std::cout << environ[i] << std::endl;
	}

	return 0;
}

『Linux』进程概念_第25张图片
libc中定义的全局变量environ指向环境变量表environ没有包含在任何头文件中,所以在使用时要用extern声明

  • 方法三使用接口getenv
char *getenv(const char *name);
参数:
	name:环境变量名。
返回值:
	存在,则返回环境变量内容,不存在返回NULL
#include 
#include 

int main(){
	// 查看环境变量HOME的内容
	std::cout << getenv("HOME") << std::endl;

	return 0;
}

在这里插入图片描述

你可能感兴趣的:(『Linux』,进程概念,冯诺依曼体系结构,操作系统)