linux--进程理解

冯诺依曼体系结构

我们常见的计算机,如笔记本。我们不常见的计算机,如大型服务器,大部分都遵守冯诺依曼体系。冯·诺依曼结构也称普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的存储器结构。
数学家冯·诺依曼提出了计算机制造的三个基本原则,即采用二进制逻辑程序存储执行以及计算机由五个部分组成(运算器、控制器、存储器、输入设备、输出设备),这套理论被称为冯·诺依曼体系结构。
在这里插入图片描述
其中:
输入单元:包括键盘, 鼠标,扫描仪, 写板等
中央处理器(CPU):含有运算器控制器
运算器:由算术逻辑单元(ALU)、累加器、状态寄存器、通用寄存器组等组成。算术逻辑运算单元(ALU)的基本功能为加、减、乘、除四则运算,与、或、非、异或等逻辑操作,以及移位、求补等操作。计算机运行时,运算器的操作和操作种类由控制器决定。运算器处理的数据来自存储器;处理后的结果数据通常送回存储器,或暂时寄存在运算器中
存储器:是用来存储程序和各种数据信息的记忆部件。存储器可分为主存储器(简称主存或内存)和辅助存储器(简称辅存或外存)两大类。和CPU直接交换信息的是主存。
输出单元:显示器,打印机等

计算机发展所遵循的基本结构形式始终是冯·诺依曼机结构。这种结构特点是“程序存储共享数据顺序执行”,需要 CPU 从存储器取出指令和数据进行相应的计算。
主要特点有:

1)单处理机结构,机器以运算器为中心;
(2)采用程序存储思想,存储程序,也就是通过计算机内部存储器保存运算程序。这样,程序员仅仅通过存储器写入相关运算指令,计算机便能立即执行运算操作,大大加快运算效率;
(3)指令和数据一样可以参与运算;
(4)数据以二进制表示,大大加快了计算机速度;
(5)将软件和硬件完全分离;
(6) 指令由操作码和操作数组成;
(7)指令顺序执行。

操作系统概念与定位

操作系统管理计算机硬件与软件资源的计算机程序(或者说是软件),同时也是计算机系统的内核与基石。操作系统需要处理如管理与配置内存、决定系统资源供需的优先次序、控制输入设备与输出设备、操作网络与管理文件系统等基本事务.

如何管理管理=描述+组织

  1. 描述起来,用struct结构体
  2. 组织起来,用链表或其他高效的数据结构

总结操作系统的理解与定位
1.硬件交互,管理所有的软硬件资源
2.为用户程序(应用程序)提供一个良好的执行环境
3.在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件

操作系统主要包括以下几个方面的功能 :
1.进程管理,其工作主要是进程调度,在单用户单任务的情况下,处理器仅为一个用户的一个任务所独占, 进程管理的工作十分简单。但在多道程序或多用户的情况 下,组织多个作业或任务时,就要解决处理器的调度、 分配和回收等问题 。
2.存储管理分为几种功能:存储分配、存储共享、存储保护 、存储扩张。
3.设备管理分有以下功能:设备分配、设备传输控制 、设备独立性。
4.文件管理:文件存储空间的管理、目录管理 、文件操作管理、文件保护。
5.作业管理是负责处理用户提交的任何要求。
在这里插入图片描述

系统调用和库函数概念

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

进程概念特征与PCB

基本概念:

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

宏观的理解:
理论角度看,是对正在运行的程序过程的抽象
实现角度看是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序

进程特征

1.动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
2.并发性:任何进程都可以同其他进程一起并发执行
3.独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
4.异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
5.结构特征:进程由程序、数据和进程控制块三部分组成。
6.多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。

程序与进程的区别

1.程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程是程序在处理机上的一次执行过程,它是一个动态的概念。
2.程序可以作为一种软件资料长期存在,而进程是有一定生命期的。程序是永久的,进程是暂时的。
3.进程更能真实地描述并发,而程序不能;
4.进程是由进程控制块、程序段、数据段三部分组成;
5.进程具有创建其他进程的功能,而程序没有。
6.同一程序同时运行于若干个数据集合上,它将属于若干个不同的进程,也就是说同一程序可以对应多个进程。
7.在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单元都是进程。

进程的描述-PCB(process control block)

进程是由进程控制块、程序段、数据段三部分组成
进程信息被放在一个叫做进程控制块(PCB)的数据结构中,可以理解为进程属性的集合。Linux操作系统下描述进程的结构体叫做task_struct
task_struct:是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
所有运行在系统里的进程都以task_struct链表的形式存在内核里。

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

查看进程信息

格式: /proc/[pid]
如:要获取PID为1的进程信息,你需要/proc/1 

大多数进程信息同样可以使用top和ps这些用户级工具来获取
通过系统调用获取进程标示符
进程id(PID) 当前进程的进程标识符getpid()
父进程id(PPID) 父进程的进程标识符 getppid()

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
	printf("pid: %d\n", getpid());    //当前进程的进程标识符
	printf("ppid: %d\n", getppid());   //父进程的进程标识符
	return 0;
}

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

运行man fork 认识fork
在这里插入图片描述
fork返回值
如果成功,则在父进程中返回子进程的PID,在子进程中返回0。
如果失败,则返回-1在父进程中,不创建任何子进程,并且适当地设置了errno
父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)

操作系统是使用双向链表结构来组织进程pcb的,
在这里插入图片描述

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
	pid_t pid = fork(); //使用系统调用创建子进程
	if(pid < 0){  //由于内存不够创建失败 子进程pcb是需要内存的
		perror("fork");
		return 1;
	}
	else if(pid == 0){ //child 子进程
		printf("I am child : %d!, fatherid: %d\n", getpid(), getppid() ); //子进程的pid 与其父进程的pid
	}else{   //father  父进程
		printf("I am father : %d!, fatherid: %d\n", getpid(), getppid() );e
	}
	sleep(1);
	return 0;
	//其中父子进程是抢占式执行的
	//在fork后,进程是从fork的下一条指令开始执行的,这是因为进程执行时是按照程序计数器
	取指令的,其保留了下一条指令,所以子进程被创建后是从下一条指令开始的,不然就一直进入入口的fork函数,进入死循环
}

在这里插入图片描述
注意多个进程的执行顺序是由系统具体的调度算法实现的
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。

进程状态

R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
僵死状态(Zombies是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
在这里插入图片描述
可以通过系统调用 kill 来结束进程

kill -9 [pid]   -9参数代表强制删除

僵尸状态如何处理呢?
可以将其父进程杀掉,不让父进程来等待这个僵死的子进程退出,而是让其变为孤儿进程,由1号进程来专门领养,在内核中回收释放该孤儿进程的pcb资源。
孤儿进程(不是状态,而是进程种类了)
父进程先退出,子进程就称之为“孤儿进程”,孤儿进程被1号init进程领养,就由init进程回收。达到释放孤儿进程资源的工作。

环境变量

基本概念
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数.如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常系统当中通常具有全局特性.
通俗来说,就是 在当前目录环境下(配置好了的)来执行编译好了的二进制可执行文件使用命令 ./fork(带了路径的)和单纯的 fork (没带路径的)结果如下图。
但是 像 ls ,cd,等命令为啥也没带路径但是也可以执行呢,就是因为他们配置了环境变量的原因。

在这里插入图片描述
常见环境变量

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

查看环境变量方法

echo $NAME   //NAME:你的环境变量名称[重点]

环境变量认识
在这里插入图片描述

环境变量的组织方式
在这里插入图片描述
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串.

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

//命令行第三个参数
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
	int i = 0;
	for(; env[i]; i++){
		printf("%s\n", env[i]);
	return 0;
}

//通过第三方变量environ获取
#include <stdio.h>
int main(int argc, char *argv[])
{
	extern char **environ;//libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
	int i = 0;
	for(; environ[i]; i++){
		printf("%s\n", environ[i]);
	return 0;
}
//通过系统调用getenv获取或设置环境变量
#include <stdio.h>
#include <stdlib.h>
int main()
{
	printf("%s\n", getenv("PATH"));
	return 0;
}

程序地址空间

以前我们认知的空间布局图
在这里插入图片描述
进程控制块对应的虚拟进程地址空间
在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 10;
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{ //father
		sleep(3);  //先让父进程睡三秒,等待子进程先执行
		printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);//值和地址
	}
	sleep(1);
	return 0;
}

在这里插入图片描述
结果显示如上图,两者地址相同,但是值却不同,以我们之前的思维想,这肯定是不可能的呀!
分析如下:
变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
但地址值是一样的,说明,该地址绝对不是物理地址(说明那就是虚拟地址咯)!
在Linux地址下,这种地址叫做虚拟地址,
我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理OS必须负责将虚拟地址转化成物理地址。

一张图理解虚拟地址与物理地址的映射及写时拷贝
页表:保存虚拟地址与物理地址的映射信息
程序中子进修改了变量的值,因为时写时拷贝,故系统有为其在虚拟地址空间对应的逻辑地址上重新映射一个(另开辟的)新的物理地址,并把原来页表中映射关系更新
在这里插入图片描述
再由此图和前面的理解来分析:同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!

发布了44 篇原创文章 · 获赞 88 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_44785014/article/details/104782506