Linux-- 进程概念的引入

Posted 川入

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux-- 进程概念的引入相关的知识,希望对你有一定的参考价值。

目录

硬件

冯诺依曼体系结构

冯诺依曼体系结构推导

重点概念

网络数据流向

软件

操作系统(Operator System - OS)

概念

定位

进程内核数据结构PCB(task_struct)

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

fork基本用法

使用if进行分流

查看运行效果


硬件

冯诺依曼体系结构

        我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。因为有些硬件的设计就是为了软件,有些软件的设计就是为了硬件,所以我们不能够将软件和硬件割裂开来学习。软件与硬件之间是会有千丝万缕的联系的。

  • 存储器:内存。
  • 输入设备:键盘、摄像头、话筒、磁盘、网卡……。
  • 输出设备:显示器、音响、磁盘、网卡……。
  • CPU(中央处理器):
    • 运算器:算术运算、逻辑运算。
    • 控制器:CPU是可以响应外部事件,协调外部就绪事件,比如,拷贝数据到内存。
关于冯诺依曼,必须强调几点:
  • 这里的存储器指的是内存。
  • 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)。
  • 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
  • 一句话,所有设备都只能直接和内存打交道。
         对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上。

冯诺依曼体系结构推导

#问:为什么会有一个存储器?


在我们看来:

  • 输入设备:是产生数据的。
  • 输出设备:是保存 / 显示数据的。

那么体系结构为:下图,不是也可以吗?

1. 存储的效率:

        CUP && 寄存器 > 内存 > 磁盘 / SSD > 光盘 > 磁盘(并且还是数量级的上升)

2. 木桶原理木桶原理又称短板理论,木桶短板管理理论

        最直接的话来说:一个木桶能够接多少水,是由木桶的最短的木板所决定的。换而言之就是,是由多个木板构成一个木桶,相当于由多个元素构成一个系统,同样的道理,一个系统的运行速度不是由最快的元素决定的,而是最慢的元素所决定。所以在计算机设计的角度来说,如果以:-> 输入设备 -> CUP -> 输出设备 -> 。来设计,这个时候输入设备和输出设备都会大大的拖慢CPU的运行速度,导致整个系统运行速度降低。更直接的来说就是:CPU运行速度太快了,对比起磁盘等外设的速度,磁盘们慢的离谱,CUP"等急了"

        于是我们在磁盘到CPU的间隔中,插入了一个内存更直接的来说就是:内存还行,比磁盘快不了太多,又比CPU慢不了太多,CPU"这个等待可以接受"利用存储器的存在,让我们对于速率上,以软件上做文章。

        通过将外设的数据预先加载到存储器当中,此时CPU读取数据时就不去外设而是去存储器读取数据。即:因为存储器的存在,以此通过软件层的策略(比如:操作系统),提升效率。以此达到以后的木桶短板就不是外设,而是内存了。例如:缓冲区满了才将数据打印到屏幕上,使用fflush函数 / 显示器的行刷新策略,将缓冲区当中的数据,都是将内存当中的数据直接拿到输出设备当中进行显示输出。

        比如:开机的时候操作系统将我们可能要访问的数据,预先从外设,读取到内存当中。

重点概念

结论:

  1. CPU读取数据(数据 + 代码),都是要从内存中读取数据。——  站在数据的角度,我们认为CPU不和外设直接交互。
  2. CPU要处理数据,需要先将外设中的数据,加载到内存。——  站在数据的角度,外设直接只和内存打交道。

        所以在语言级的学习中的一局话,我们就可以理解了:程序(在文件、磁盘中)要运行,必须先加载到内存中(因为体系结构决定)

网络数据流向

        我们利用QQ和远在其他地方上大学的朋友,进行聊天,并且使用的是由冯•诺依曼体系结构搭建的电脑。

        我们在与朋友聊天的时候,我们通过键盘,输入自己想说的话,于是键盘输入的信息会加载到内存,而我们也需要知道我们输入的是什么,所以信息还会回显屏幕上。而关键在于对方收到我们的信息,所以CPU需要将我们输入的内容,经过网络的包装、打包、网络IP等,然后打包完的内容写回到内存当中。然后通过网卡进行网络上的数据传输,然后对方通过网卡接受到我们所发出的信息,由于体系结构,就会将内容加载到内存当中,然后CPU进行读取分析,将分析完的数据写回到内存,然后将内容写到显示器上。

路线:

        冯诺依曼体系结构,能够帮助我们理解对用的日常生活中的软件行为的,是硬件规定了我们的软件应该怎么做

软件

操作系统(Operator System - OS)

概念

        任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
  • 内核(进程管理,内存管理,文件管理,驱动管理)。
  • 其他程序(例如函数库,shell程序等等)。

定位

在整个计算机软硬件架构中,操作系统的定位是:一款纯正的 "搞管理" 的软件。是为了:
  • 对上:提供良好的使用环境  ——  是目的
  • 对下:通过管理好软硬件资源,保证系统的稳定性  ——  是手段

可以说:给用户提供一个稳定、安全、简单的执行环境。

进程内核数据结构PCB(task_struct)

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

task_ struct内容分类

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

#问:为什么要存在PCB(task_struct)?

        重点理解操作系统的作用,以及操作系统如何做管理的。要管理必须要先描述再组织,先描述就是面向对象,对于被管理的对象先抽象成类,然后再组织,就是根据类来定义对象。然后将对象,根据某种链入式的结构组织起来。

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

fork基本用法

        fork之后,代码是父子共享的,但是如果父子多做的事情都以一样的,那我们费这么劲作用不大,需要的是分出来做不同的事情,这样可以明显的提高效率。所以通常用法,使用fork是利用父子进程执行不同的代码。

使用if进行分流

        简单来说就是,父子进程都能看见fork后面的代码,但是只能执行自己的。

int main()

    pid_t id = fork(); //pid_t相当于无符号整数
    if(id<0)
        //创建失败
        perror("fork");//打印出fork失败的原因(由C语言提供)
        return 1;
    
    else if(id == 0)
    
        //child process(task)
        while(1)
        
            peintf("I am child, pid %d, ppid %d\\n", getpid(), getppid());
            sleep(1);
        
    
    else
    
        //parent process
        while(1)
        
            peintf("I am father, pid %d, ppid %d\\n", getpid(), getppid());
            sleep(1);
        
    
    printf("you can see me!\\n");

    sleep(1);

    return 0;

        利用id在父进程里面是子进程的pid,在子进程里面是0。让else if与else里面的语句同时执行。主要是一个变量(id)是可以有不同的值。与语言本身无关,仅仅就是因为使用fork创建了子进程而产生的特殊现象。(不同值的原理:在进程地址空间讲解)

查看运行效果

        用于可以直接清晰的查看进程。

ps xaj | head -1

        就是打印一个表格的头标(表格中数据的名称) 

ps axj | head -1 && ps axj | grep myproc

        &&与逻辑与相同,只有左侧的执行成功才会执行右侧的,要成功答应,需要左侧与右侧都成功。

ps axj | head -1 && ps axj | grep myproc |  grep -v grep

while ;: do ps axj | head -1 && ps axj | grep myproc | grep - v grep; sleep 1; done 

  • 为什么给子进程返回0,给父进程返回子进程的pid?(此处没有官方的讲解,仅仅是理解)

        任何一个子进程,永远都只有一个父进程,任何一个父进程,可以有一到多个子进程。父进程 : 子进程 = 1: n,只要父进程调用fork之后就可以有子进程。而为了父子进程便于协同,所以,就相当于生活中:父亲不可能叫自己的孩子为,那个孩子吃饭了、那个孩子别乱摸……,更何况还会有多个孩子。所以,需要给子进程进行标识,等同于fork之后,给父进程返回子进程的pid,以此父进程来对子进程进行相关管理。

        而对于子进程永远都只有一个父进程,子进程与父进程的对应关系就是相当明确的、唯一的。子进程可以很方便的找到父进程。

  • 创建进程的时候,操作系统要做什么?fork为什么会有两个返回值?

        创建子进程,那么我们一定要给子进程配套一个task_struct,来让操作系统来管理这个新进程。这样这个 task_struct 就可以入到系统的全局的维护进程列表的结构当中,操作系统就可以对新进程进行管理了。

        对于在系统层的fork实现中,return时核心代码的执行状态:

         操作系统和CPU运行某一个进程,本质从 task_struct 形成的队列中挑选一个task_struct,来执行它的代码。

        进程调度,变成了在task_struct的队列中选择一个进程的过程。只要想到进程,优先想到进程对应的task_struct。

        所以当准备return的时候,核心代码已经完成了。因为return,是为上层返回结果。既然已经返回结果,证明计算过程已经结束。

        而通过此我们知道父与子进程的执行其实是有优先级的。

        所以,才会看到会有两个返回值。不是出了fork才有的父子进程,在fork中的return的时候,进程早已经创建出来了,甚至子进程都可以进行调度了。

#问:父子进程被创建出来,哪一个进程先运行?

        有可能父进程刚刚将子进程创建出来,父进程就因为某些原因,就被放到了(run_queue)队列的尾部,反而放到了子进程的后面,这个时候就是子进程先运行了。但是也有可能,系统一口气将父进程跑完了,甚至将fork之后的代码也跑完了。所以这个时候:哪一个先跑完是不可控的。

        谁先运行,不一定,这个是由操作系统的调度器决定的。只有操作系统最清楚谁先调用。

Linux----进程概念(下)

7. 环境变量

①概念

环境变量(environment variables)

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

在windows中也有环境变量,快捷键win+r可以直接打开的都是在path里添加好绝对路径


②Linux常见的环境变量

注意:程序,命令,指令,可执行程序… … 都是一个概念

1.PATH

指定命令的搜索路径

当我们运行我们已编译好的可执行程序时有一个./表示当前路径
如果我们不带./系统会报错command not find
但系统命令ls cd 等就可以,这是因为系统在PATH(辅助系统进行指令查找)的帮助下能够找到
有两种方案

  1. 将自己的可执行程序添加到系统路径下
    命令:cp 程序 /usr/bin/
  2. 将当前的路劲添加到PATH
    用指令:PATH=$PATH:路径

注意:环境变量是在用户登录时从配置文件加载的,所以方法2在重启后不会保留
环境变量的信息在bashrc里面

2.HOME

指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)

3.SHELL

当前Shell,它的值通常是/bin/bash常见shell还有sh,ksh,csh等

4.HISTSIZE

可以保存历史命令的最多条数

③其他

1.环境变量的函数

头文件<stdlib.h>

函数功能
char *getenv(const char *name)获取环境变量
int setenv(const char *name, const char *value, int overwrite)设置环境变量

2.本地变量和export

本地变量:只能在当前shell命令行解释器内被访问,不能被子进程继承
环境变量:具有“全局属性”,可以被子进程继承
export 本地变量将本地变量导为环境变量

3.set和env

env只能显示环境变量
set可以显示环境变量和本地变量

4.内建命令

内建命令:shell程序内部的一个函数
echo export
shell执行的命令的是内建命令时就直接调用内建命令的方法(这也是为什么echo export可以使用本地变量),不是时会再去调用fork()创建子进程子进程不可继承本地变量

5.环境变量组织方式以及如何获取环境变量

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


获取方式

  1. libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明
    例如:
int main(int argc, char *argv[])

	extern char **environ;
	for(int i=0; environ[i]; i++)
		printf("%s\\n", environ[i]);
	return 0;

  1. 通过命令行参数(详见下面

8. 命令行参数

main函数有三个参数:int argc, char *argv[], char *env[]
Linux中的指令有那么多选项也是通过命令行参数来实现的

  1. int argc和char *argv[]:argv[]指针数组元素个数是argc

有如下代码

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

	for(int i=0;i<argc;i++)
	        printf("%s\\n",argv[i]);

  1. 第一个元素是 ./test
  2. 第二三四个是接下来的dd ff gg
  3. 最后一个元素是NULL

  1. char *env[],接收环境变量
int main(int argc, char *argv[], char *env[])

	for(int i=0; env[i]; i++)
		printf("%s\\n", env[i]);
	return 0;


9. 初始进程地址空间 (重点,概览)

在C和C++学习的时候我们看到有关内存的操作时会联想到下面这张图

下面有一段代码用来验证各个区域的划分

printf("code addr         :%p\\n",main);
const char *p="hello world";
printf("p(read_only)      :%p\\n",p);
printf("global(g_val      :%p\\n",&g_val);
printf("global(g_unval)   :%p\\n",&g_unval);
//stack
int a=0;
int b=0;
static int c=0;
int d=0;
printf("stack addr(&a)    :%p\\n",&a);
printf("stack addr(&b)    :%p\\n",&b);
printf("stack addr(static &c)    :%p\\n",&c);
printf("stack addr(&d)    :%p\\n",&d);
//heap
char*q1=(char*)malloc(10);
char*q2=(char*)malloc(10);
char*q3=(char*)malloc(10);
char*q4=(char*)malloc(10);
printf("heap addr(q1)      :%p\\n",q1);
printf("heap addr(q2)      :%p\\n",q2);
printf("heap addr(q3)      :%p\\n",q3);
printf("heap addr(q4)      :%p\\n",q4);

printf("args addr(args[0]):%p\\n",argv[0]);
printf("args addr(args[argc-1]):%p\\n",argv[argc-1]);
printf("env addr(env[0])  :%p\\n",env[0]);

①引入虚拟内存

下面有一段代码

printf("At begin g_val is: %d\\n",g_val);
pid_t id=fork();
if(id==0)
//child
	int count =0;
	while(1)
	
	    printf("child pid: %d, ppid: %d, g_val=%d, [&g_val=%p]\\n",
	    						getpid(),getppid(),g_val,&g_val);
	    sleep(1);
	    count++;
	    if(count==5)
   		g_val=100;
	

else if(id>0)
//parent
	while(1)
	
	    printf("parent pid: %d, ppid: %d, g_val=%d, [&g_val=%p]\\n",
	    						getpid(),getppid(),g_val,&g_val);
	    sleep(1);
	

else
	//TODO


可以看到:父子进程的g_val值不一样(代码共享,数据各自私有一份:写时拷贝)
但为什么它们有相同的地址[0x601048]呢?

  1. 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
  2. 但地址值是一样的,说明,该地址绝对不是物理地址!
  3. 在Linux地址下,这种地址叫做 虚拟地址
  4. 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理

②虚拟内存

<冯诺依曼>规定: 程序运行的时候,代码和数据一定在物理内存上
而将虚拟地址转化为物理地址的操作由OS完成

在32位下, 操作系统默认会给给个进程构成一个地址空间的概念(0000…00——FFFF…FF) 4GB的空间

在Linux中描述进程的是task_struct,而描述进程空间是用的一个结构体mm_struct
下面是Linux2.6.39的源码

形象表现如下图
而完成虚拟内存到物理内存的映射的工作由页表完成
页表:是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系。 每一个进程都拥有一个自己的页表,PCB表中有指针指向页表

有了这种映射关系,虚拟内存完全可以完全一样


那么为什么不直接访问内存呢

  1. 如果直接进程访问物理内存,那么看到的地址就是物理地址,使用指针造成越界(越界不一定报错,关键字:金丝雀保护机制)
  2. 进程的独立性,无法保证
  3. 因为物理内存暴露,其中就有可能有恶意程序直接通过物理地址,进行内存数据的篡改,也可以读取

虚拟地址到物理地址的-一个转化,由OS来完成的,同时也可以帮系统进行合法性检测
每个页表项会有许可位来控制对一个虚拟页面内容的访问,如果一条指令违反了许可条件,CPU就会触发保护机制,Linux Shell一般将这种异常报告为段错误(segmentation fault)
例子C语言中对字符常量的修改


为了会进行回收等操作,内存需要知道某个进程的退出内存,所以内存管理模块和进程管理模块是强耦合的,
管理只需要知道那些内存区域(page)是无效的,哪些是有效的将内管管理 所以和进程管理进行解耦
底层类似智能指针,每次申请空间就会给一个count变量++,回收一次count就–当count为0就进行解耦

为什么进程地址空间是按照区域划分的
在磁盘中的可执行程序,本身是按文件的方式组织成一个个的区域

链接过程更方便
由于程序在物理空间内的存放位置完全是根据当前内存状态存放的,为了能让进程(PCB)找到 ,进程地址空间也进行了区域划分,通过页表将所有的数据整合起来,使在地址空间看到的和在磁盘看到的是同一种物理排序
有了地址空间,我们就可以在确定的位置,执行代码的入口,完成运行
物理内存是以页为单位的,一页为4kb,而程序中的每一个块被分为若干个4kb,一个4kb叫做页帧


回到最开始的问题:为什么父子进程的g_val地址一样

  1. 当创建子进程的时候,子进程的task_struct,mm_struct ,页表等都会以父进程为模板创建,(默认继承父进程的大部分属性)
  2. 父子进程共享一份代码,控制语句if else 控制父子进程执行的代码块
  3. 父子进程的进程地址空间里的虚拟地址&g_val一定一样,当子进程想对g_val进行写的时候,操作系统发现进程只有读权限,于是操作系统在物理内存开辟一个新的和g_val一样的空间,将g_val拷贝进去,同时让子进程的页表映射到这块空间,同时具有写权限
  4. 在上层看来虚拟地址是一样的,只是映射到的物理地址变化了,本质上进行了写时拷贝

③总结

进程和程序的区别:(宏观)
进程包括:task_struct, mm_struct 页表代码和数据


四个问题:

  1. 进程地址空间究竟是什么

地址空间本质是进程看待内存的方式,抽象出来的一一个概念,内核struct mm. struct. 这样的每个进程,都认为自己独占系统内存资源
区域划分本质:将线性地址空间划分成为一个七个的区域[start, end]
虚拟地址本质:在[start, end]之 间的各个地址叫做虚拟地址

  1. 为什么要存在地址空间?

保护物理内存,不收到任何进程内的地址的直接访问,方便进行合法性校验
将内存管理和进程管理进行解耦
让每个进程,以同样的方式来看待代码和数据

  1. 验证地址空间的基本排布

(待补充)

  1. 地址空间vs物理内存之间的关系

虚拟地址:和物理地址之间是通过页表完成的映射关系

以上是关于Linux-- 进程概念的引入的主要内容,如果未能解决你的问题,请参考以下文章

Linux----进程概念(下)

Linux 内核进程管理 ( 进程特殊形式 | 内核线程 | 用户线程 | C 标准库与 Linux 内核中进程相关概念 | Linux 查看进程命令及输出字段解析 )

linux篇linux进程(下)

linux篇linux进程(下)

linux篇linux进程(下)

linux C/C++多进程教程(多进程原理以及多进程的应用)(getpidfork)(未完成)