进程

  • 前言
  • 一、预备知识
    • 1.1 什么是操作系统
    • 1.2 为什么要有OS
  • 二、进程
    • PCB的内容
    • 进程切换
    • fork(系统调用接口)
    • 操作系统进程状态
    • Z- -僵尸进程
    • 僵尸进程的危害
    • S+/S 的区别
    • 孤儿进程
    • 进程优先级
  • 三、环境变量
    • 第一层:是什么
    • 第二层:为什么要有环境变量
    • 第三层:怎么用
    • 第四层:可以被子进程继承
    • 第五层: main函数如何获得环境变量
  • 四、进程地址空间
    • 进程地址空间究竟是什么?
    • 为什么要存在地址空间?
    • 地址空间和物理内存之间的关系?
  • 总结

前言

在进入进程之前,我们一定要先了解一下操作系统,本章节会用一些生动的例子讲述什么是操作系统,如何简单理解进程,什么是环境变量,什么是进程地址控制,涉及少量的操作,但对于后面进程控制的学习做了一个非常重要的铺垫


一、预备知识

1.1 什么是操作系统

我们先从狭义上了解一下操作系统,狭义上指的是操作系统内核本身。广义上通常指的是内核,bash,图形化界面,接口,甚至预装的软件等等。

内核分为:进程管理,文件管理,驱动管理,内存管理。

操作系统是一款进行软硬件管理的软件

我们用生活当中的例子来说明

校长:管理者(主要做决策) 辅导员:执行者(主要做执行) 学生:被管理者

假设校长是操作系统,那么我们的教室,黑板,都是其中的硬件,校长今天想要更新我们学校的设备,那么这个过程就是软件(校长)对硬件(设备)做管理。但是有一点很重要,管理者和被管理者并不会直接交互,生活中校长和学生并不会经常交互,但是他是有在管理你的,通过什么途径呢,假设校长要发奖学金,校长并不会跑到你的宿舍问你的成绩,而是通过辅导员(执行者)来管理学生,他只需要像辅导员下发指令即可。然后他就不关心被管理者的,所以他不需要直接和被管理者打交道,而是通过信息来做决策。

从上面:管理是通过信息去进行管理的。

信息从哪里来:假设一位学生从高中到大学,它的信息就会被录到大学的系统当中,这个信息的录入就是我们的辅导员(执行者)做的。

要多少信息:这个就要看描述学生需要多少信息(取决于被管理的对象)。在这个过程我们需要先描述一名学生,比如学生档案中有一个学生的姓名,年龄等等,这样有了描述学生的方法之后,来一个学生就通过之前的模板写入相应的被管理者信息就可以了;在linux内核当中要描述就可以使用struct来描述被描述的对象,当中放的就是对象的一些属性和特性。

struct stu{char name[];char addr[];cahr telphone[];double score;
}

从上面:管理需要先描述(struct)被管理者 ,然后对象就可以按照相应的内容写入即可。
当然我们无法百分百说明一个人的特性,所以我们这里都是用一些特征。

当然,被管理的对象是很多的,上万个学生,难道我们定义上万个结构体,那我们如果想要查找其中数学最好的人给他发奖学金的话,我们需要相应的数据结构。如果这时我们要让张三同学退学,然后我们的管理方式是链表,相当于校长对于链表结点进行一个删除操作,管理的步骤就变成了对于某种数据结构的增删查改。

这个时候我们前面所学的链表,队列,堆等等数据结构就可以将被管理者管理起来了。我们这时候就可以知道了,我们的管理者是对信息进行管理的,管理是通过数据结构,而信息又是通过执行者等放入到我们对应的数据结构当中。对学生的操作变成了对数据结构(链表等等)的操作。

操作系统做管理:先描述(struct),后组织(数据结构)!!!

在linux下校长(管理者)相当于操作系统的角色,辅导员(执行者)相当于驱动程序,学生(被管理者)就是硬件或者是被管理的软件。

如何理解冯若依曼体系

冯诺伊曼的体系结构中都是硬件与硬件之间的关系,那我们重新梳理一下操作系统如何管理硬件。首先:操作系统不会直接和硬件打交道,操作系统就要描述各种各样的硬件,然后把硬件用某种数据结构链接起来。比如说:操作系统想要管理硬盘,内存,都需要对应的结构体。描述一样东西我们通常用它对应的属性,如描述磁盘,我们可能关注磁盘的大小,厂商,当前容量,当前的设备状态。删除硬件实际上就是把它的对应的数据结构删除,将它的资源做清理。

1.2 为什么要有OS


早期的操作系统用的是人直接与硬件打交道的方式,与他的开关直接做输入输出。通常都是科学家来做的,再往后面才有命令行交互,图形化界面。

结论:

  • 操作系统是方便用户使用的。
  • 操作系统对上,给用户,给开发人员提供良好的运行环境。
  • 操作系统对下,管理好底层的相关软硬件资源。 如果你的操作系统不能很好的利用资源,假如你的内存16g,但是他每次最大只能使用2个g给你,那么他就没有充分高效的使用软硬件资源。

如何理解操作系统在计算机中作用
操作系统对下:底层硬件部分支持冯诺依曼的体系结构,驱动程序,就是当你插u盘时,操作系统要安装驱动程序(执行者),就如学校多了一个学生,操作系统就需要获取相关信息,然后才能做管理。这样子操作系统才能够管理到我们的硬件部分。

其中的系统软件部分指的就是我们的操作系统,而其中的操作系统则是我们之前说明的狭义上的操作系统。

操作系统对上操作系统不信任任何用户,所以用户不可以直接和我们的操作系统,驱动程序,底层硬件打交道;任何对系统硬件或者软件的访问都必须通过操作系统的手,用户只能通过从上到下的方式去访问底层的软硬件。

类似这种场景,操作系统是银行的窗口,底层硬件/软件是银行的金库,用户肯定不能直接去金库里面拿钱,必须得通过排号,银行的窗口才能够取钱。

上面的system call(操作系统提供的接口,函数调用(C接口))就类似银行的窗口。我们日常写的一些小程序,如hello world之类的,这些要用到底层硬件的,都会贯穿整个体系结构。但是如果你在本地实现一个add函数,就不会贯穿整个体系结构了。

为什么system call上面封多一层用户操作接口
其中的用户操作接口,就是因为使用系统调用接口的难度也还是很大,因为你要了解这个接口大概是干嘛的,要怎么用也是成本很高的,所以也就封装了一层用户操作接口。我们经常谈论的C语言,C++就只用到了开发操作和lib,也就是用户和用户操作接口这个阶段。比如我们之前写的链表,二叉树,都是用户级的代码,是不会贯穿整个体系结构了。

C/C++的用处怎样最大
从冯诺依曼,我们的C/C++程序大多停留在存储器和运算器之间交互。我们要让语言发挥更大的价值,比如网络程序等等,我们就要用到输出设备,网卡等等的资源,这时候语言的价值才大了。

总结:计算机体系是一个层状结构,任何访问硬件和系统软件的行为,都必须通过OS接口,贯穿OS进行访问操作。

如何理解库函数是否为第一手的库
现在我们就由两种函数了,一种是库函数,一种是系统提供的接口,这两者如果一定要产生某种关系的话,那么一定是一个上下级的关系,库函数在体系结构的上面。我们所使用的C库站在系统接口来看,就是二次开发的库,但是如果站在用户层开发的角度,我们用的就是第一手,由语言给我们的库。

二、进程


首先操作系统能不能开启多个程序?肯定可以,打开你的任务管理器你能看到很多程序,这就像校长(操作系统)与学生(进程)之间的关系,若进程(被管理者)很多的是时候,他们伸手向操作系统要资源,OS要管理这些进程,怎么管理这些运行起来的程序(进程)呢?

答案:先描述,后组织 --多次强调这句话是因为这句话非常重要,描述即用struct把属性信息写入,组织即用数据结构将定义的变量维护起来。

内核中的描述方式 --PCB
所以进程要起来,首先要把磁盘当中的程序加载到内存当中,系统当中会为每个进程创建描述进程的结构体(PCB),每个进程都有一个PCB(进程控制块),即struct task_struct结构体,这个PCB当中几乎包含了与进程相关的属性信息(上下文,pid等等)。

简单理解:进程 = 你的程序 + 底层的数据结构(这里是PCB)
小总结:操作系统对进程的管理,最终转化为了操作系统对进程信息的管理,也就是在内核当中对双链表进行增删查改。所以PCB的存在也就是为了对进程进行管理。


1.为什么要有PCB:操作系统要进行管理工作,先描述,在组织。
2.什么是PCB:进程控制块,是用来描述进程的属性集合。
3.组织方式是什么?

PCB的组织方式
组织方式是使用自己实现的双链表,在task_struct中放了一个节点,节点内放了前后指针,其中运用了offsetof和container_of通过这个节点就能得到task_struct的指针!

PCB的内容


  • 1.标识符:进程pid,通常可以用数字来表示进程的id。ppid为父进程的id。
    认识一条命令: ps ajx - - a表示所有,j表示任务,x表示输出
    这里解释一下,第二行是我们的当前运行的程序,第三行是grep本身这条命令它包含了我们刚刚运行的程序。可以用-v就可以让grep不出现。

  • 2.状态:任务状态,退出代码,退出信号等

  • 3.优先级:进程占用某种资源(CPU,磁盘,网卡)的时候,相对于其他进程的优先级。这个资源你是肯定能用上的,但是你是先得到还是后得到的问题。优先级的出现是因为资源太少,而用的人太多了的问题。所以,优先级本质 是资源有限的前提下确立谁先/后访问资源

  • 4.程序计数器:保存的是即将被执行的指令的地址,是一个寄存器。

CPU的核心工作流程:1.取指令(eip决定的),2.分析指令,3.执行指令(再回到1)。指令是c/c++代码经过编译,汇编,就会有二进制的指令了。CPU依次识别读到CPU当中。CPU运行的代码,都是进程的代码,那么CPU如何得知应该取进程中的哪行指令。
答:CPU当中有个寄存器(eip),叫做pc(point code)指针,保存档期正在执行指令的下一条的地址。

  • 5.内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针,通过PCB当中的内存指针找到对应进程的代码和数据。cpu看到的是pcb信息,cpu要执行的不是属性信息,而是代码和数据。

  • 6.I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

  • 7.记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。比如说我们的年龄就是一个记账信息。

  • 8.上下文数据(重点):进程放在cpu上运行,产生临时数据,进程在CPU上运行会有很多寄存器上的临时数据,这些数据在进程被剥离的时候,是需要被保存的,这些数据被称为上下文数据。

cpu内的寄存器数据,称之为进程的硬件上下文

状态:除0,有状态寄存器。eip下调指令的地址

//通过调试,进入反汇编,再到调试窗口中打开寄存器,即可观察
//这些都是当前进程所产生的的临时数据,cpu内的寄存器数据,称之为进程的硬件上下文
EAX = 00DC5588 EBX = 00AF3000 ECX = 00000001 EDX = 00DCA5D0 ESI = 00871023 EDI = 00871023 EIP = 00871920 ESP = 0097F960 EBP = 0097F97C EFL = 00000202

从窗口–>寄存器,我们可以看到若干条关于寄存器的信息。
从中我们的第一条指令未执行的时候eip指向的就是我们的00871920.

进程切换


那么有一个问题,我们的进程不是一直在cpu上跑的,当要把当前进程从cpu上剥离下来,该怎么做呢?
举个栗子:
比如在生活中,大一大二的时候有人来学校招兵,有人就跑去当兵了了,跑去当兵,然后他如果身体不适,又跑回来学习。但是回来的时候发现自己已经退学了。因为自己忘记了保留学籍这个过程,也就是将我们的学习状态保存起来。之后回来的时候,你还要将保存的学籍恢复

需要进程切换的情况:
所以当我们的进程在运行的时候,因为某些原因(时间片,或者更高优先级进程来了)需要被暂时停止执行,让出cpu,所以这个时候需要进程保存自己的所有的临时数据(即当前进程的上下文数据)。
当然这里的话是pcb要剥离的话就由pcb将它的临时数据带走,和上面的例子有点点偏差,但是可以方便理解。

如何进行切换:
PCB想要保存应该怎么做呢?其实也很简单,PCB是一个结构体变量,他只需要定义一个相关的属性信息,存进去就可以。下一个进程来到cpu面前,直接覆盖式的将自己的临时数据写到cpu当中就可以。上下文保护和进程切换就完成了。

PCB并不是只是由全局的链表维护起来,比如他在cpu当中的运行队列也会有他,甚至
一般一个cpu只有一个运行队列,一个等待队列。所有的结构都是由内核当中的操作系统完成的。
所以pcb可能和文件,内存,各种各样的设备关联起来,所以内核内一定存在的大量的pcb指针的信息。

示意图:1.由于当时间片到了,就要从cpu上剥离下来,这时cpu中硬件上下文可以放在pcb当中,而恢复的过程就从pcb保存上下文的地方放回cpu继续调度。
2.当来了一个优先级更高的进程(当OS是支持抢占)也会让当前运行的进程从时间片剥离。

现在的理解:程序要运行:首先从磁盘加载到内存当中,在内核当中把相应的信息用pcb管理起来,然后要运行的话,还要将排查表存到cpu当中的运行队列,然后该进程就会由cpu当中周而复始的调用起来。假如你运行着qq,然后时间片到了之后,qq被剥离之后,可以重新在链入到运行队列的末端,然后就可以实现一个周而复始的进程调度了。

并行vs并发:
并发:假如是单CPU,单核:跑多个进程时,通过进程快速切换的方式,在一段时间内,让所有的进程代码都得到推进!伪同时
并行:多个CPU,真正的多个进程同时工作。真同时

查看进程id:

getpid为系统调用接口,可以查看自己的pid,page2的都是系统接口

从图可以得知 ,命令行解释器是创建子进程进行执行任务的。能从ls /proc看到所有的进程。

注意::pcb在操作系统内,在内核区不暴露给用户。

fork(系统调用接口)


fork的头文件是#include<unistd.h>4

  • 运行man fork认识fork
  • fork有两个返回值
  • 父子进程代码共享,数据各自私有一份。

我们先用一下再说!

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{printf("I am a father! pid : %d\n",getpid());fork();while(1){printf("I am a Process ! pid :%d , ppid :%d \n",getpid(),getppid());sleep(1);}return 0;
}

我们可以看到父进程的ppid是27612,这个实际上就是bash(命令行解释器),而观察pid为29349的ppid是29348,可以看到他们中间存在父子关系。

理解fork: 本章简单理解,下一篇会更加透彻说明。

  • 程序员角度: 父子共享用户代码,而用户数据各自私有一份。 用户代码是只读的,不可修改;所以大家都可以用,但是数据要私有一份(写实拷贝)。否则比如你今天关闭你的qq,你的网易云关掉了,这合吗?这不合理。所以进程是具有独立性的(即进程之间不能互相干扰,所以用户数据必须各自私有一份)!!!
  • 系统角度:进程 = 我的程序代码+ 内核数据结构。所以创建子进程,通常是以父进程为模板,其中,子进程默认使用的是父进程的代码和数据(写时拷贝),而子进程的task_struct,如子进程pid,调度时间,记账信息等不会拷贝父进程的数据,其他大部分信息都会拷贝进去。所以子进程通常使用的是父进程的代码,而没有修改数据时,用的也是父进程的数据。

fork的返回值:通过man手册。我们可以看到pid_t的返回值是0返回给子进程,若创建失败,-1返回给父进程,返回子进程的pid给父进程。那么父进程是什么类型呢,我们用grep -ER "pid_t " /usr/include 这个命令找,我们可以知道他是一个int,但是在系统当中实际上他是一个无符号整数。

即使是父进程跑完的代码,实际上子进程当中也能看到,只不过不会再去执行罢了。

fork的返回值:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{printf("I am a father! pid : %d\n",getpid());pid_t id = fork();if(id == 0){//childwhile(1){printf("I am a child ! pid :%d , ppid :%d \n",getpid(),getppid());sleep(1);}}else if(id > 0){//fatherwhile(1){printf("I am a father! pid :%d , ppid :%d \n",getpid(),getppid());sleep(1);}}else {//error}return 0;
}

说明一下,这里father每次都先运行,这个是由系统的调度器决定的,并不是固定的。

早在c/c++时候,我们是没有办法看到两个死循环同时运行的。这里有两个返回值在这里可以验证出来。我们在通过其他的角度来理解一下。

两个返回值的原因:

当一个函数即将return的时候,他的主要功能其实是已经完成了的。那么走进fork这个函数的时候,当我们的return之前,实际上父子进程同时都会执行后面的代码,所以最终返回值是有两个的。

pid_t fork()
{//创建子进程的操作//... 子进程已经创建出来了甚至放到了调度队列当中,这个时候的子进程已经是以父进程为模板,使用者父进程的代码和数据了//所以接下来的return代码实际上父进程和子进程都会去执行,而不同的身份他们对应的返回值不同,最终导致两个返回值不同。return xx;
}

为什么给子进程返回0,给父进程返回子进程的pid:

其实这就就像生活当中的关系,父亲可能有多个儿子,而每个儿子一定只有一个父亲。假设儿子们分别叫张三和李四,那么父亲喊儿子做事的时候一定会指定是叫张三,或者李四。而如果张三或者李四找父亲有事,只需要喊一声爸爸,这种关系就能被确立起来了。

并且pid是给我们程序员去看的,在内核当中是用链表关系来维护父子关系的。所以父进程的PCB不会保存子进程的pid。

如何创建多进程:用循环!

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
void doSomething()
{int count = 0;while(count++ != 5){printf("pid : %d, ppid :%d , count : %d \n",getpid(),getppid(),count);}
}
int main()
{printf("I am a father process : %d\n",getpid());pid_t ids[5];//共享资源for(int i = 0;i<5;++i){ids[i] = fork();//写时拷贝if(ids[i] == 0){//childdoSomething();exit(1);}}printf("------------------------------------\n");for(int i = 0;i< 5;++i){printf("%d ",ids[i]);}printf("\n");getchar();return 0;
}

操作系统进程状态



这个图是操作系统上面描述的状态,放在任何一款操作系统上面都是正确的。
而我们需要具体学习,则需要一个具体的操作系统,我们这里用linux说明。
根据内核版本2.6.32.27:

/*1. The task state array is a strange "bitmap" of2. reasons to sleep. Thus "running" is zero, and3. you can test for combinations of others with4. 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 */  gdb调试程序的时候,在断点处停下来。"Z (zombie)",     /* 16 */  僵尸状态 "X (dead)"     /* 32 */      死亡状态
};

1. R状态:R状态并不是一定在cpu上面运行,进程在运行队列中,都是R状态。所以表示进程准备好了,可以被调度。
用一段代码测试一下:

int main()
{while(1) {printf("1");}return 0;
}


我们查看一下这个状态,却发现该进程处于S(睡眠),而不是R(运行),为什么呢?我们的程序难道没有在运行吗,实际上是因为我们的程序大多数的时间处于等的一个状态,IO的时间占的时间比重大,即使我们测试多次他也在S状态。修改一下代码,重新测试。

int main()
{while(1) {}return 0;
}


这样子就能够看到运行状态啦。

2. S:睡眠状态:意味着进程在等待时间完成,也可以被叫做可中断睡眠。上面的例子已经出现,IO输出,或者程序主动sleep都会进入睡眠状态。用ctrl+c或者是kill命令都可以让该进程停止休眠
3. D:磁盘休眠状态,有时也叫做不可中断睡眠状态,这个状态的进程通常会等待IO的结果。只能等待PCB自己醒来,或者关机了(有概率宕机)。

讲述D状态前,讲述一个小故事,当有一天,PCB需要进行IO操作时,它将数据给了硬盘当中的搬运工,倘若这个时候该PCB进入S(可中断式睡眠状态)。这个时候他就在内存当中等待,因为他需要搬运工告诉他是否数据搬运成功,不成功的话数据应该如何处理,这个时候,操作系统看到内存紧张,看到无所事事的PCB(等待IO结果),就跟他说内存资源紧张,一下子就把他杀掉了。这个时候搬运工搬运过程当中发现硬盘内存满了,于是带着数据回来,却找不到之前的PCB了,这个时候可能就会造成数据丢失,搬运工也要去完成其他PCB的IO指令。
那么这个时候锅由谁背呢?内存?操作系统?硬盘?

答案:操作系统,因为内存就那么大,怪不了他;而磁盘只是一个打工人,他只是接收命令之后进行操作;这个时候操作系统就不能删除在等待IO结果的进程,就算内存要满了,也要找其他PCB下手,那么这样一个不能被操作系统删除的状态就是,D状态(硬盘休眠状态)了。

不管是深度睡眠还是浅度睡眠都是等待状态。
D/S状态用途例子:
比如今天QQ想往网卡发送数据,但是网卡太忙了,那么就可以把QQ设置为S状态,这样内存拥挤,操作系统删掉qq上的数据,我们只需要重新发送就好了。
比如今天硬盘太忙了,那么可以把当前进程设置为D状态。

4. T状态:某种原因需要当前进程等待,用kill -19 可以停止,kill -18让该进程继续进行。

执行下述代码,进程进入R/S状态。

int main()
{while(1) {printf("I am running\n");sleep(1);}
}

我们可以通过kill信号来停止进程(19号信号),18号信号让进程继续工作。


随之的状态也变成T状态。

5. X死亡状态:这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
例子:倘若今日有个人不慎在你身旁经过,忽然到底身亡,这个时候你打电话叫警察和120,警察(OS)判断没有刑事案件的可能,120说是脑溢血。这个时候你才能恢复正常生活。

总结:我们要确定一个进程的“死亡”原因,进程退出不是立马就让OS回收资源,释放进程的所有资源。而是收集进程退出时的相关有效数据,如退出信息(进程退出时会写入PCB),供操作系统和父进程进行读取。这个时候进程退出但没被释放资源的状态就是僵尸(Zombie)状态。直到被操作系统或父进程读取成功,才会是X(死亡)状态。

Z- -僵尸进程


在父进程未通过系统调用接口来回收子进程的退出信息时,子进程属于僵尸进程
做个实验:用while :; do ps axj|head -1 && ps axj|grep ./test;sleep 1; echo "##################"; done来检测进程的状态。
然后我们使用我们之前写的循环创建子进程的代码,最后能够看到,父进程不退出,子进程就一直是Z状态。

操作系统描述的状态落实到linux上也能解释了:

僵尸进程的危害


父进程一直不去读取,PCB一直存在,这样子内存当中就会被占用内存。

所以一个正确的流程是,父进程给子进程派发任务,子进程拿到任务去执行任务,然后由父进程去读取子进程的退出信息。

父进程该怎么做?
父进程通过wait或者waitpid就可以获得进程的退出信息。

S+/S 的区别


前台进程运行,bash命令行边不会帮我们解释,我们输入命令也没用,这个时候进程会带上+号,表示我们是一个前台进程。
在./test &加上这个&之后变成后台进程,用ctrl+c无法取消,前台也可以输出命令。

孤儿进程


当父进程退出,而子进程资源没有被回收,这个时候就会被1号进程(system d(centos 7.6)操作系统)领养
因为当孤儿再退出,操作系统就可以把孤儿进程的资源再释放了。其中父进程会被父进程的父进程给回收,所以父进程不会变成孤儿。

int main()
{if(fork() > 0){sleep(1);exit(1);//父进程先退出}while(1);//子进程不退出
}

且进程一旦变成孤儿进程,(R+ --> R)会自动从前台进程变成后台进程

top命令查看一号进程,root启动的,就是操作系统。
0号进程在操作系统启动的时候存在。然后fork创建子进程成1号,然后0号进程就退出了。
centos 7.6 的1号进程叫做systemd,centos7.5 的1号进程叫做init。

如果一个进程是Z(死亡)/D(深度休眠)状态,可以用9号SIGKILL掉吗?

不行!原因:一个人已经死了,就不能再杀了它了。

进程优先级

是什么?
是得到某种资源的前后顺序。
为什么存在优先级?
本质是因为资源有限。
怎么用?

命令 ps -al可以看到当前所有进程的信息。
Linux的优先级由pri决定。ni(nice)可以修正数据,当pri优先级的数值越小,优先级越高。反之,跟排名类似。
但是优先级不能一味的高或者一味的低,NI值在[-20,19]之间。
因为OS的调度器也要适度的考虑公平问题,不然会有进程处于饥饿问题

当我们写完代码形成的exe,双击运行本身就是在linux下./成为进程跑起来。
为什么要将可执行程序加载到内存当中才可以运行。原因:冯若依曼体系。

我们进程的PRI就是我们最终进程的优先级。NICE值是标识进程可被执行的优先级的修正数值。
当NICE为正就是要调低优先级,反之。
NICE的取值范围是[-20,19]。

PRI vs NI --》不是相同的概念,NI是修正的数值。

怎么调整优先级:
top命令,r(renice),然后就是调整NICE值,注意每次都是以80这个基准线+/-NICE值。

为什么PRI(new) = PRI(old–>80)+NICE,为什么PRI(old)要固定成80。
原因:1. 在一个基准值下,方便调整(因为要调整说明之前的值你不满意)。
2. 如果PRI(old)是新的值,那么我们可以通过不断重复的调整优先级,让PRInew(超过低于60或者超过99),如果这样设置,操作系统会把优先值弄得比较复杂,所以设计上有比较简单,满足需求的方法,自然也就选择他了。

为什么NICE值是可控的呢?
因为操作系统的调度器,要“公平”且较高效进行调度。所谓公平:不是平均!!并不是每个进程的时间片特别平均,而是根据进程的特性,分配对应的时间。所以要较为公平的调度,NICE值在统一水平的就会差不多的运行。

uid

ll -n就可以显示一个用户的id信息,在linux系统中,这是标识一个用户的方法,这就类似于你的QQ当中的QQ号。这就类似于用户的uid。而不是一个昵称,昵称并不能标识好一个具体的人。

uid出现的原因:计算机比较擅长处理数字数据。处理其他的信息都是转换成数字再处理。

  • 并行:多个进程运行在多个cpu上,同时进行运行,并称之为并行。所以有可能在任意一个时刻,有两个或者多个以上的进程运行。
  • 并发:多个进程在一个cpu上采取进程切换(保存上下文)的方式,在一段时间内,让多个进程都能推进。
  • 独立性:多进程当中代码共享(代码只读),数据各自私有一份(写时拷贝)。例子:QQ的关闭不会让网易云笔记也退出。
    我们可以创建一个子进程,发生除零错误后退出,观察父进程是否还存在。
    代码:

监控脚本:

结果:

  • 竞争性:资源少,进程多,进程之间自然有了竞争属性,为了高效的完成任务,更合理竞争相关资源,便有了优先级。
    假设你有了竞争性,实际上你的优先级也就比较高了。

三、环境变量


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

第一层:是什么

概念:环境变量一般是指操作系统中用来指定操作系统运行环境的一些参数。
如:我们在编写c/c++代码的时候,在连接的时候,我们从来没有指定过我们所需要的链接的动态静态库在哪里,但是一样是可以链接成功,生成可执行程序,实际上就是有相关的环境变量帮助编译器进行查找,

并且环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。

windows当中存在一些系统级的变量,假如你有用过java,那么你们一定配置过环境变量。

第二层:为什么要有环境变量


那么linux下的环境变量:
PATH是一个系统级的环境变量。查看一个环境变量的命令叫echo $PATH
如果想查看系统当中的大部分环境变量,可以用env

对于我们的一个可执行程序,我们通常的运行方式是./xxxx。实际上这个xxxx也是一个命令,但是为什么我们要加入./才可以使用呢?

实际上,我们通常听到的:程序,命令,指令,可执行程序都是一个概念。本质上和我们的ls是没有区别的。实际上ls也是一个可执行程序(x)。

那么我们运行ls,为什么不是用./ls,实际上./是一个当前路径,而我们运行我们的程序(假设proc)为什么会提示命令找不到呢,找不到说明他曾经找过。系统曾经找过这条命令,系统是去哪里找呢?
答案:就是在环境变量当中,就是我们上面echo $PATH当中找到的,也就是他辅助我们的系统进行指令查找。

第三层:怎么用



PATH当中是以 作为间隔符,其中当中就是一个个决定路径,我们执行ls这样的指令,就会在这几个路径当中查找,找不到就说命令找不到(command not found)。

那么我们如何proc就可以运行呢?
方法1:把proc放到PATH路径(任意一个)当中的下面,sudo cp proc /usr/local/bin
方法2:将当前的路径添加到环境变量当中。PATH=$PATH:(当前路径),然后我们就可以在PATH当中查看当我们的路径,然后就可以直接proc运行了。当然这种方法是一次性的,你退出登入后登进就没有这个路径了。因为他是在系统运行的时候通过脚本然后加载我们的环境变量的。
/usr/bin下放着很多的可执行程序

通过ls -al可以查看到我们的环境变量信息在bash_profile,bashrc当中。


PATH来源:配置文件/系统开机时就会从配置文件导入我们的服务器中。

HOME:
普通用户和root的用户的家目录不同。
/home/ljh与/root,根本原因系统里面有一个环境变量叫做$HOME。不同的身份都不同。
不同的机器上的环境变量不同。一台linux可以由多个人同时使用。

SHELL:当前用的命令行解释器的种类。/bin/bash
支持的shell版本 :

echo $HISTSIZE :history:记录着之前的命令 history |wc -l (行数) ,在命令行按上下就可以访问到。

在命令行上运行的大部分指令,它的父进程都是bash!
即bash创建子进程,子进程执行你的命令。

第四层:可以被子进程继承


getenv获取环境变量本质上就是传参过去的。
命令行中我们可以定义两种变量,MY_VAL=“you can see me”,这种就是本地变量
查变量都可以用echo $MY_VAL,本地变量只能在当前shell命令行解释器内被访问,不可以被子进程继承。

即在vim当中使用getenv(“MY_VAL”)是找不到的(段错误)。原因:proc程序一运行就是我们bash的一个子进程,然后MY_VAL是本地变量,没有被继承,即子进程找不到的。

本地变量转变环境变量(全局属性–>可以被子进程继承) 采用export命令。
export MY_VAL,再运行proc,就可以被子进程获取到了。

env 当中只有环境变量(env|grep my_val),没有本地变量。可以用set查看本地变量。set |grep MY_VAL;
set既可以查看本地变量也可以查看环境变量。


当我们定义一个环境变量 val=100,我们用echo $val,可以查看到,echo命令创建子进程却可以用本地变量。说明了echo的父进程不是bash。echo是一个内建命令,可以理解为shell程序内部的一个函数。
原因:echo在shell内部建立,命令在shell内是一个函数。所以echo可以用本地环境变量。

第五层: main函数如何获得环境变量


在代码层面上,获得环境变量,main函数当中有三个参数。其中有argc,argv,env。
argc是命令行参数的个数,argv是一个指针数组,其中./proc也是一个。

int main(int argc,char *argv[])
{for(int i = 0;i<argc; ++i){printf("%s\n",argv[i]);}return 0;
}

main函数的前两个参数称之为命令行参数
argv的存放方式
为什么要存在命令行参数?
让同一个程序通过不同的命令行参数实现不同的功能。也就是帮助我们设计出同一个程序,可以设计出不同的业务功能!!!

第三个参数是char* env[],是一个环境变量。通过数组传参的方式拿到了对应的环境变量。
之前的继承就是通过这个参数来实现。(全局属性)。说到底也就是传参。从而实现环境变量被子进程继承下去。从而实现全局变量的全局属性。

getenv的原理实际上也就是在env当中进行文本匹配。
获得环境变量的其他方法:extern char**enviorn,使用的时候先声明,我们就可以如下使用(main可以不带参数)。

for(int i = 0;environ[i];++i)
{printf("%s\n",environ[i]);
}

enviorn的原理解释:
下面代码,1和2传过去了吗?传过去了,压入在show当中的栈帧,但是show当中我们没办法直接拿到,但是我们其实是可以通过一些指针操作来访问到1和2。同样的道理,main函数当中没有参数,但是可以把argc,argv,env的参数压入main当中的栈帧,然后我们environ就可以通过指针操作访问到main函数当中的这些信息。

void show()
{}
int main()
{show(1, 2);return 0;
}

四、进程地址空间



先验证一下空间布局的情况!!
代码:


结论:
字符常量区和代码区不给被修改那么一开始如何写入?
字符常量区和代码区对于用户来说,在页表当中的权限只有读的权限,当你的权限转换为内核,内核是可以更改字符常量区和代码区当中的数据的。

结论:
进程地址空间,不是内存。
进程地址空间,会在进程的整个生命周期内一直存在,直到进程退出。

进程地址空间究竟是什么?


地址空间本质是进程看待内存的方式,抽象出来的一个概念,内核mm_struct,每一个进程都有一个结构体,每个进程都认为自己独占系统内存资源(每个私生子每个人独占100亿)。
区域划分:本质就是将线性进程地址空间划分成一个个area,[begin,end]

虚拟地址本质:在[start,end]之间的各个地址叫做虚拟地址。

为什么要存在地址空间?


若没有,倘若发生越界,修改了其他进程的代码和数据(进程的独立性无法保证)。内存暴露给进程是一个十分危险的行为。并且内存上的所有内容都能被一个恶意程序全都能读取上了。并且这样较为困难去实现权限管理。所以虚拟地址空间可以在页表上面有一个权限的表,标定了可读,可写等等。虚拟地址到物理地址的转化,就可以由操作系统来完成,同时就可以对于这个过程的合法性做检查。
指针越界,一定会报错吗?
不一定,从进程地址空间来看,当我们访问栈区的内容时,我们可能越界后还在栈区,我们还是能进行读写的,但是若越界到了静态区,代码段等等,页表是能够判断我们是否有写权限,操作系统发现我们没有写权限就会终止我们进程,给我们报错。

1.即保护物理内存,不受到任何进程内的抵制的直接访问,方便合法性检测。
2.当你的指针在栈区,越界之后修改之后,如果不是之前的设置的特殊值就会被识别。(金丝雀技术)。
3.根据[start,end]来确认一个有效范围,是一个粗一点的考量。页表则可以进行映射级别的权限管理(rw)。如在常量区当中的字符串数据,如果我们尝试修改,页表的权限发现我们没有写权限,但是我们进行了修改,操作系统就会终止我们的程序。
4.将内存管理和进程管理进行解耦
5.让每个进程以同样的方式,来看待代码和数据。(顺序流程)
6.可执行文件影响了地址空间

例如static,是语言指导编译器做的工作。而有些(字符常量区)不能被修改则是操作系统的概念。

进程管理和内存管理解耦好处:(延迟加载)
当有一个16GB的游戏性需要加载到我们的电脑时,我们的物理内存假设是4G的,我们是可以正常运行的。原因就是因为当数据加载到内存的时候进程也是一条条指令执行的,所以我们的内存管理模块可能就先将16G当中的200M数据加载到内存当中,等到这些数据被进程跑完的时候,再重新覆盖式的加载到先前的物理内存当中,只需要更改一下页表的映射,这个时候进程就能够一直在跑,所以站在进程的角度,他并不知道内存是怎么样管理的,但是他能够高效的运转!!

编译链接的需要:
当我们有一个可执行程序在我们的磁盘上面时,它的文件也是按照某种格式分为.code,.data,.bss(未初始化数据区),.readonly(只读),我们的可执行程序,本身就划分成为了一个个的区域!!因为这种格式的便于生成可执行程序的,有一个链接的过程!就相当于把你的全局数据区和库的全局数据区合并,其他也一样合并起来。所以我们进程地址空间区域才有了区域划分!!
物理内存的内存分配的按页,一页是4kb。磁盘上可执行文件的区域基本也是4kb及其整数倍。
磁盘上的一个个数据叫页帧,内存上的叫页框。

使内存管理和进程管理解耦合
操作系统主要进行四大管理:进程管理,内存管理,驱动管理,文件管理。
其中进程管理和内存管理用如今的进程地址空间+页表的形式能够实现解耦合!
我们可以认为操作系统进行进程管理时就是对PCB,地址空间和页表进行管理即可,而内存管理只需要对页表和物理空间进行管理,内存管理模块只需要知道哪一块物理内存是否有存放即可,这里我们可以采用智能指针的方式(可以理解为每一块内存(page)有一个计数器count,有一个页表的内容指向该page,就计数器++,当计数器为0时我们就释放该物理内存)。

早期没有进程地址空间,直接对物理内存进行访问时,对于进程管理和内存管理也是可以实现解耦合,但是这一定会带来电路上的复杂,所以这也算一个以前的直接访问物理内存不可取的原因之一。

为什么在malloc出来的内存空间需要用指针接受?
原因就是从物理内存申请到的空间与页表建立映射,然后我们的进程地址空间的堆上面就要记录这个数据,如果你弄丢了这个数据,那么我们的进程也就找不到了这个数据。

地址空间和物理内存之间的关系?


地址是一样的,如果我们看到的是物理地址,那么是肯定不可能的。对同一块物理内存读取数据,一定不可能是两个值的。所以之前在语言层面上的地址(以及引用的概念)都不是对于物理内存。而是一种虚拟地址。而虚拟地址是由操作系统提供的。

从语言上:因为数据和代码一定要在物理内存上的(冯诺依曼体系)决定,一定要将数据和代码加载到物理内存上),我们需要将虚拟地址转化成为物理地址
从系统上:所有的程序,都必须运行起来,运行起来之后,该程序就成为了进程,打印的地址与进程是有关系的。虚拟地址与进程有着某种关系。

虚拟地址和进程之间的关系(例子):
用一个例子说明,今有一个富豪,他手上有100亿元,他有10个私生子,对于每一个儿子伸手找他要钱,这个富豪都会给,但是如果每一个儿子都找富豪要100亿,那么富豪自然也是给不起的,但是儿子们一般都是1w,1w的要钱。站在儿子的角度,他们每个人都认为以后自己能有100亿元。
在这个例子当中,富豪相当于操作系统,它的100亿元相当于物理内存,而一个个私生子,他们就相当于进程,
操作系统会默认给每个进程构成一个地址空间的概念。而这里富豪给儿子们画的100亿元的家产相当于一个当前进程的地址空间。当然这是儿子与父亲的一个共识而已,代码并没有说明。
当然,就算这个富豪没有100个亿,而他跟私生子说他有100亿,那么对于私生子而言,他们也会认为他们有100亿元,所以,即便物理内存小于4GB,在32位操作系统(000…~FFF…)地址空间为4GB,也可以映射到相应的物理内存,只不过要多的时候给不出来而已。

页表出现的意义:
页表的出现肯定不是出于效率上的考量,因为如果进程能够直接访问物理内存速度绝对比通过进程地址空间后经页表转化的方式要快!但是页表带来的好处也有不少。
当磁盘上的可执行程序加载到内存上面的时候,可以随意放置(因为内存碎片化,如果要让物理内存只能顺序存放的话,在效率上会有所损失),然后通过改变页表当中的映射关系,让进程地址空间可以顺序使用的情况下,又能够让我们更加方便的使用物理内存(即站在进程的角度,取指令是顺序的,站在操作系统角度,管理物理内存的时候,可以随便放置内存)。
所以虚拟地址是可以完全一样的。因为每一个进程都拥有一张表,通过映射关系就可以找到对应的物理内存。
写实拷贝的时候父子进程映射到同一段代码,而数据拷贝一份则改变页表的映射关系即可。

总结

守护进程大家可以自行了解一下。大佬写的博客如下:
https://www.cnblogs.com/yyxayz/p/4070216.html
对于进程的简单的知识铺垫的到此为止,由于博主本身的知识受限,讲的不好的地方望大佬指出批评。喜欢的话不妨一键三连,支持下博主。

【Linux】用最形象的例子学习进程,从入门到深入相关推荐

  1. Linux(CentOS-7)-全面详解(学习总结---从入门到深化)

    目录 Linux概述 Linux特点 Linux应用领域 Linux和Windows区别 Linux下载安装 安装VMWare虚拟机 下载CentOS 安装CentOS Linux三种网络配置 背景 ...

  2. linux学习课程从入门到精通:Centos8-系统进程管理

    本人从事IT行业已有十多年,有着丰富的实战经验,总结了大量的学习方法,更是积累了很多的学习资料,很高兴能在这里跟大家交流学习,希望能在这里跟大家共同进步和成长! 全套学习资料移步至公众号[学神来啦]更 ...

  3. Linux与C++11多线程编程(学习笔记)

    多线程编程与资源同步 在Windows下,主线程退出后,子线程也会被关闭; 在Linux下,主线程退出后,系统不会关闭子线程,这样就产生了僵尸进程 3.2.1创建线程 Linux 线程的创建 #inc ...

  4. Linux的内核设计与实现之进程管理(含源码)

    Linux内核设计与实现--进程篇之进程管理 目录 概述 进程与线程 进程管理 进程描述符及任务结构 进程状态 进程上下文 线程创建 写时拷贝 fork() vfork() 创建线程 内核线程 进程终 ...

  5. linux中reap用法,ATT汇编学习笔记(一)

    file命令使用介绍 file最常用的场景就是用来查看可执行文件的运行环境,是arm呢,还是x86呢,还是mips呢?一看便知$ file a.out a.out: ELF 64-bit LSB ex ...

  6. linux中怎么退出执行过程,(进程)处理过程中的Linux:从执行到退出

    Linux是一个多任务操作系统,表面上看,同时运行许多任务--即进程.每一个进程都在系统中留下足迹.这里介绍一些检查这些足迹的工具,并且还要说明蔓延的/proc目录到底是什么. 欢迎归来.上周我们考察 ...

  7. linux c编程项目实例,Linux c编程实例_例子

    例一:字符与整型变量的实现 #include int main() { int c1,c2; char c3; c1='a'-'A'; c2='b'-'B'; c3='c'-; printf(&quo ...

  8. Linux该如何学习(新手入门必看)

    本节旨在介绍对于初学者如何学习 Linux 的建议.如果你已经确定对 Linux 产生了兴趣,那么接下来我们介绍一下学习 Linux 的方法. 如何去学习 学习大多类似鹿丁解牛,对事物的认识一般都是由 ...

  9. Linux操作系统学习笔记【入门必备】

    Linux操作系统学习笔记[入门必备] 文章目录 Linux操作系统学习笔记[入门必备] 1.Linux入门 2.Linux目录结构 3.远程登录 3.1 远程登录Linux-Xshell5 3.2 ...

最新文章

  1. CentOS7关闭防火墙
  2. python语言开发的软件有哪些-最适合人工智能开发的5种编程语言,你知道几种?...
  3. 深度学习训练中关于数据处理方式--原始样本采集以及数据增广
  4. 结对开发四------求一维无头数组最大子数组的和
  5. jupyter notebook使用opencv的例子_Python安装Jupyter Notebook配置使用教程
  6. 将SQL-SERVER逆向工程导入Power-Design中并给表的字段添加注释
  7. java发送消息_通过java给qq邮箱发送信息
  8. 动态规划——K号数(蓝桥杯试题集)
  9. 嵌入式开发之davinci--- MSB和LSB
  10. oracle数据库赋权_oracle数据库删除赋权
  11. 第二节:Maven的运行机制
  12. STM32CubeMX使用(一)之实现点灯点灯
  13. bpsk调制及解调实验_5G调制解调原理:从入门到放弃?
  14. 柯尼卡美能达一体机 扫描文件,不是全彩的,就首页和尾页是彩色,中间黑白
  15. 服务器单硬盘raid,服务器硬盘做raid0
  16. java实现阿里云图片文字识别
  17. 物品分类游戏html5,幼儿物品分类教案
  18. CET eve 看星星
  19. 读取xslx文件(一)
  20. xinxin--小爱同学

热门文章

  1. SLAM导航机器人零基础实战系列:(三)感知与大脑——5.机器人大脑嵌入式主板性能对比...
  2. 最高月薪15K!当过老师、卖过保险的退伍小哥,用三个月开启技术人生!
  3. 超级实用案例,Python 提取 PDF 指定内容生成新PDF
  4. Julia中从Git时出现超时问题的解决方法---(例如:安装GR、Rmath一直超时)
  5. 手机端网页技术--使自己做的asp.net网页适应手机浏览
  6. win10激活错误,软件授权服务报告无法激活计算机怎么办?
  7. java catch块_用Java编写带有清除操作的catch块
  8. 2023新版php仿蓝奏云网盘合集下载页面系统源码 带后台版本 源码搭建
  9. 【深度之眼cs231n第七期】笔记(二十七)
  10. 一种简单、安全的Dota全图新思路