Linux下的进程概念相关知识点整理总结

1.认识冯诺依曼系统

冯诺依曼体系结构是我们当代计算机所遵守的硬件结构
这个硬件结构在我们的主流计算机上面都是遵守的
而我们冯诺依曼基本结构主要包括:
在这里插入图片描述
计算机硬件构成:
磁盘,cpu,显卡,内存,网卡,键盘,显示器,打印机等等
这些设备都是独立的一个个硬件设备,他们都有各自的功能分类。
一般情况下可以将计算机设备分为:四类
1.输入设备:键盘,磁盘,显卡,话筒,网卡,摄像头等
2.输出设备:显示器,磁盘,显卡,网卡,音响等
3.存储器(内存)
4.运算器和控制器((cpu)中央处理器):

延伸:CPU不直接和外设打交道,因为CPU很快,外设很慢。因此有存储器在二者间起缓冲作用。在数据层面,任何外设,基本优先对内存读写;CPU也是直接对内存读写,内存是体系结构的核心设备,IO = input + output。

2.操作系统概念与定位

操作系统(Operator System)

概念:
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:

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

目的:

  1. 与硬件交互,管理所有的软硬件资源
  2. 为用户程序(应用程序)提供一个良好的执行环境

定位:
在整个计算机软硬件架构中,操作系统的定位是: 一款纯正的“搞管理”的软件

图解:
在这里插入图片描述

3.深入理解进程概念,了解PCB

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

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

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

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

组织进程:
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里
在这里插入图片描述
结论:曾经所有的程序启动,本质上都是在系统上面创建进程。
有了进程控制快,所有的进程管理任务与进程对应的代码和数据毫无关系,与内核创建的该进程的PCB强相关。
把进程控制块PCB用双向链表组织在一起,于是操作系统对进程的管理,变为对数据的管理,本质上就是对双链表的增删查改。

进程控制块中的上下文数据:
进程的代码是不可能在很短时间运行完的,规定每个进程的时间片(单次运行的最长时间),用户感受到的多个进程同时运行,本质上是CPU的快速切换。CPU只有一套寄存器,为了保护上下文,进程的这些临时数据被写入在PCB中,再来执行时,恢复上下文

4.进程状态,创建进程,僵尸进程和孤儿进程其形成原因和危害

a.查看进程

写一段程序Myproc.c:隔1s打印"hello",
./运行,同时复制SSH渠道再打开一个窗口,便于监视进程。

查看进程代码:
ps aux | grep a.out | grep -v grep
在这里插入图片描述

b.以文件形式查看进程

./a.out
在这里插入图片描述

延伸:
cwd:这就是为什么文件操作时,不指定路径,会默认在当前目录下创建文件。

c1.查看fork()创建子进程相关内容

#include<iostream>    
#include<sys/types.h>    
#include<unistd.h>    
    
int main()    
{    
  fork();    
  std::cout << "hello proc:" << getpid() << " hello parent:" << getppid() << std::endl;    
  sleep(1);                                                                                                   
  return 0;    
} 

在这里插入图片描述

c2.简单理解fork创建子进程

目前创建进程主要有两种方式,./cmd或run command,fork在操作系统角度,和它们没有差别。

fork本质是创建进程,系统中多了一个进程,就多了一份与进程相关的内核数据结构 + 进程的代码和数据。 我们fork只是创建了子进程,但是子进程对应的代码和数据呢?

默认情况下,子进程会“继承”父进程的代码和数据
1.代码:fork之后,产生的子进程和父进程代码是共享的。代码是不可被修改的,这意味着父子代码只有一份完全共享。
2.数据:默认情况下,数据也是“共享的”,不过修改时会发生写时拷贝来维护数据的独立性。
子进程内核的数据结构task_struct,也会以父进程的为模板初始化自身

关于返回值:(为什么会有两个返回值)
如何理解一个函数有两个返回值?return时子进程已被创建,return也是语句,父子都会执行。

我们创建的子进程和父进程干一样的事情吗?这是没有意义的。
一般是通过if-else分流,让父子进程各自执行不同的代码段,而这就是通过fork的返回值来完成的。
⭐️创建失败:<0
⭐️创建成功:给父进程返回子进程的PID;给子进程返回0,表示成功创建。

返回值是数据,return时需要写入。谁先返回,就会发生写时拷贝,可以看到两个返回值的确不同。
注:fork之后,父子谁会先运行?这是不确定的,是由调度器来确定的。

d.进程状态

进程的状态信息也是在task_struct(PCB)中。进程状态的意义在于,方便OS快速判断进程,并完成特定的功能,比如调度。本质上是一种分类。

R运行状态(running)
运行状态不一定在占用CPU,只是表示当前进程在运行队列中,随时可以被CPU调度。

#include<iostream>      
#include<sys/types.h>      
#include<unistd.h>      
      
int main()      
{      
  while(true);    
  return 0;    
}  

在这里插入图片描述

S浅度睡眠状态(sleeping) ,也叫做可中断睡眠(interruptible sleep)
当完成某种任务是,任务条件不具备,需要进程进行某种等待(S/D)。可以随时接收信号[Ctrl + c]掉
我们把运行状态的task_struct从运行队列(run_queue)放到等待队列(wait_queue)中,叫做挂起等待(阻塞)
把从等待队列放到运行队列中,被CPU调度,叫做唤醒进程
注意:千万不要认为,进程只会等待CPU资源。进程可能会因为运行需要,在不同的队列里,所处状态就不同,本质上进程状态就是一种分类。

#include<iostream>    
#include<sys/types.h>    
#include<unistd.h>    
    
int main()    
{    
  while(true)    
  {    
    std::cout << "Always" << std::endl;                            
  }    
  return 0;    
} 

在这里插入图片描述

可以看到大多数处于睡眠状态,还有少部分在运行状态 ——
这是因为,打印到显示器上,显示器是外设,很慢,IO等待外设就绪是要花时间的。而CPU太快了,挂起运行挂起运行特别快,虽然给人感受一直在运行,实际上相当长的时间都在休眠。
(这也是为什么刚才要看到R状态时,只写了一个空语句,因为这样没有IO,不用等待,排队CPU资源即可)

D深度睡眠状态(Disk sleep),也叫不可中断睡眠状态(uninterruptible sleep)
进程处于D状态,不可以被杀掉!(很难演示) ,在这个状态的进程通常会等待IO的结束

T暂停状态(stopped)
可以通过发送 SIGSTOP 信号给进程来停止进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

延伸:
前台进程和后台进程的区别:
前台进程:./myproc,输入指令无效,[ctrl + c] 可终止进程
后台进程:./myproc &,可以执行指令,[ctrl + c] 不能终止进程,退出进程要用kill

X死亡状态(dead)
回收进程资源。进程相关的内核数据结构&代码和数据

Z僵尸状态(Zombie)
为什么要有僵尸状态?因为需要辨别退出/死亡原因,把进程退出的信息(数据)写入到task_struct中,供系统/父进程读取

e僵尸进程和孤儿进程

僵尸进程:
https://blog.csdn.net/sakeww/article/details/123483922
孤儿进程:
https://blog.csdn.net/sakeww/article/details/123485370

5.了解进程调度, Linux进程优先级,理解进程竞争性与独立性,理解并行与并发

5.1进程优先级

为什么会有优先级?
资源太少!本质是分配资源的一种方式。
即:通过确认优先级,来保证资源分配

帮助理解:
计算机里面有很多进程,这么多进程都要被调度,这些进程最终都要能够集合优先级来确定使用CPU的顺序
磁盘,网卡,显卡,各种资源也有这种情况
所以我们在实际中见到各种各样的队列,CPU的运行队列,等待某种资源的等待队列,实际上都是确认优先级,就是为了先后申请空间。
进程的PCB中也包括一个字段,叫做优先级,它用来确定进程在调用时先还是后还是CPU

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

5.2查看并修改优先级

在这里插入图片描述
输入指令:
top
r
30847 (我们需要改变的pid)
数字
q(退出)
在这里插入图片描述
系统为了防止我们多次修改优先级,所以这次我们加上sudo
sudo top
(输入你的相关信息)
r
30847 (我们需要改变的pid)
数字
q(退出)
在这里插入图片描述
延伸:
输入数字的时候也可以设负数
最开始的优先级是80,他的范围是:[60,99]

问题:
nice的值为何是一个相对较小的范围呢?
答:优先级再怎么设置,也只能是一种相对的优先级,不能出现绝对的优先级
否则会出现很严重的进程“饥饿问题”
“饥饿问题”某一个进程长时间得不到资源
调度器:较为均衡的让每个进程享受到CPU资源

延伸:

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

5.3 相关名称介绍

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

5.4 PRI和NI

  1. PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值小进程的优先级别越高
  2. 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
  3. PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice
  4. 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
  5. 所以,调整进程优先级,在Linux下,就是调整进程nice值
  6. nice其取值范围是-20至19,一共40个级别

6.理解环境变量,熟悉常见环境变量及相关指令, getenv/setenv函数

6.1环境变量的概念

引出问题:
我们知道命令,程序,工具,,,本质都是一个可以执行的文件,
gcc生成的a.out 也是命令,那么我运行的时候需要./?
./的作用就是:帮系统确认对应的程序在哪里,
为何系统的命令不用带路径呢?环境变量

基本概念:

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

6.2查看环境变量的方法

查看环境变量方法:
echo $PATH
在这里插入图片描述
路径以:进行分割,分割出多条路径,
查找规则:
先找第一个路径,然后第二个路径,,,当找到路径的时候,就会停止寻找,将这个路径下的可执行程序跑起来

延伸问题:
我们在执行命令的时候(./a.out)我们不想带./该如何实现?
答:上面那么多路径,将a.out拷入随便一个路径里面即可
(不推荐,会出现问题:会污染命令池)
export PATH=$PATH:(当前路径)
接下来就可以按照我们的需求进行执行了

则可推出:
我们安装软件就是:将软件拷贝到这些默认路径下面

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

语言上面定义变量本质是在内存中开辟空间(有名字)
不要去质疑OS开辟空间的能力!!!
环境变量本质是:OS在内存/磁盘中开辟的空间,用来保存系统相关的数据!!!
变量名+变量内容

和环境变量相关的命令:

  1. echo: 显示某个环境变量值
  2. export: 设置一个新的环境变量
  3. env: 显示所有环境变量
  4. unset: 清除环境变量
  5. set: 显示本地定义的shell变量和环境变量
    延伸一点:
    系统上还存在一种变量,是与本次登录(session)有关的变量,只在本次登录有效,(本地变量)
    export | 命令(将本地变量导入环境变量,xshell关闭后,也会消失)

main函数是可以携带函数的

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

在这里插入图片描述
在这里插入图片描述
./a.out -a -b -c
这里都可以看作字符串
“./a/out” “-a” “-b” “-c”
argc决定有几个有效命令行字符串,最后一个是NULL

也可以这样

#include <stdio.h>
int main(int argc,char *argv[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}

但是不建议:
一般数组传参建议将数组的个数带上,有时候我们传了一个数组但是就是只想要前几个,,,

为什么要命令行参数?
我们在执行ls命令时,我们会后带一些参数
同一个程序,我们可以带入不同参数,来让他呈现出不同的表现功能

#include <stdio.h>      
#include<string.h>      
#include<unistd.h>      
      
int main(int argc, char *argv[])//命令行参数      
{      
  if(argc != 2){    
    printf("Usage:%s -[h|l]\n",argv[0]);                                                                                                                                                                         
  }                                                                                                                            
  if(strcmp(argv[1],"-h") == 0 )                                                                                                             
    printf("hello -hhh\n");                                                                                                                  
  else if(strcmp(argv[1],"-l") == 0)                                                                                                         
    printf("hello -lll\n");                                                                                                                  
  else printf("hello sakeww!\n");                                                                                                            
  return 0;                                                                                                                                  
}                                                                                                                                            
~      

在这里插入图片描述
即:
指令有很多选项,用来完成同一个命令的不同子功能
选项底层使用的就是我们的命令行参数!!!

6.3环境变量获取的三种方法(知道即可,不需要深究)

main函数除了可以传递命令行参数也可以传递环境变量,
在这里插入图片描述
方法一:

#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;    
  }    

可以发现env不需要传递个数
因为这是系统给我们传的,参数个数是不用判断的
此时运行你的函数就可以运行env了

方法二:

#include <stdio.h>
#include <unistd.h> //内部定义了这个environ
int main()
{
    extern char **environ;
    int i = 0;
    for(; environ[i]; i++){
        printf("%s\n", environ[i]);
    }
    return 0;
}

问题:
函数没有参数,可以传参吗?
可以,获取这个数据的方式就要使用特殊方法了

方法三:
在这里插入图片描述在这里插入图片描述
即便我们可以获取环境变量,但是我们很大部分情况也不会获取环境变量,因为环境变量我们一般情况下很少使用,只不过概念比较重要,需要知道这里的知识点

6.4环境变量通常是具有全局属性的

在这里插入图片描述
在这里插入图片描述
当我们将这个进程不断的运行几次后,发现它的pid会改变
因为我们的进程是不断创建的
ppid一直不变
ppid 是 -bash
命令行启动的进程,父进程都是-bash ,子进程就是我们的程序,如何启动呢?fork()
子进程的环境变量是系统给的,也可以理解为父进程给的
bash是从系统里面读的,系统的环境变量在系统的配置中
当你的bash登录的时候,系统就已经将环境变量导入你的上下文管理器里面了
所以我们env显示的都是shell导入在自己上下文的环境变量,这个环境变量是可以继承给子进程的
可是如何证明:
这样的变量叫做本地变量
在这里插入图片描述

环境变量是可以被子进程继承的
既然可以被继承,本地变量能被子进程继承吗?
上图表示这个环境变量是存在的,但是我们env寻找一下
在这里插入图片描述
发现找不到,即这个变量现在还不是环境变量
在这里插入图片描述
在这里插入图片描述
发现结果显示为空
综上所述,我们在命令行定义的就只是一个本地变量

export将这个变量导入到环境变量也就是父进程bash的环境变量列表中
在这里插入图片描述
即:此时环境变量已经导给了父进程,覆盖已经应有的环境变量
子进程./我们的程序在运行的时候,他的环境变量会继承父进程,也就是父进程给传的env参数
,也就是父进程给子进程新传递了一个变量,刚刚我们获取不成功,现在就直接获取成功了
在这里插入图片描述
环境变量具有全局属性,根本原因就在于环境变量可以被子进程继承的
小总结:
环境变量具有“全局属性”本质:环境变量可以被子进程继承下去!
环境变量影响了整个“用户系统”

7.理解C内存空间分配规律,了解进程内存映像和应用程序区别, 认识地址空间。

堆,栈,堆栈是什么?
答:堆是堆,栈是栈,堆栈是栈

#include <stdio.h>      
#include<stdlib.h>      
#include<string.h>      
#include<unistd.h>      
      
int g_unval;//未初始化      
int g_val = 100;//初始化      
      
int main()      
{      
  const char *s = "hello sakeww";      
  printf("code adder:%p\n",main);//代码区,main函数一定在代码区,那么他的地址一定也在      
  printf("string rdonly addr: %p\n",s);//字符常量区      
  printf("uninit addr:%p\n",&g_unval);      
  printf("init addr:%p\n",&g_val);      
      
  char *heap = (char*)malloc(10);      
  printf("heap add: %p\n",heap);//堆区      
      
      
  printf("stack addr:%p\n",&s);//栈区      
  printf("stack addr:%p\n",&heap);      
      
  int a = 10;      
  int b = 20;      
  printf("stack addr:%p\n",&a);      
  printf("stack addr:%p\n",&b);                                                                                                                                                                                  
  return 0;                                                                               
}  

在这里插入图片描述

#include <stdio.h>    
#include<stdlib.h>    
#include<string.h>    
#include<unistd.h>    
    
int g_val = 100;    
    
int main()    
{    
  //数据是各自私有一份                                                                                                                                                                             
  if(fork() == 0)        
  {                      
    //child              
    int cnt = 5;         
    while(cnt){                                                                    
      printf("i am child,times:%d , g_val = %d, &g_val = %p",cnt,g_val,&g_val);    
      cnt--;             
      sleep(1);          
      if(cnt == 3)       
      {                                                                
        printf("###############child更改数据##################\n");    
        g_val = 200;                                                    
        printf("##############child更改数据done################\n");    
      }                                                                                                                                      
    }                                                                                                                                        
  }                                                                                                                                          
  else{                                                                                                                                      
    //parent                                                                                                                                 
    while(1)                                                                                                                                 
    {                                                                                                                                        
       printf("i am parent,g_val = %d,&g_val = %p\n",g_val,&g_val);                                                                          
       sleep(1);                                                                                                                             
    }                                                                                                                                        
  }                                                                                                                                          
  return 0;                                                                                                                                  
}       

在这里插入图片描述
问题:为什么上述的所有地址都是相同的
如果C/C++打印出来的地址是物理内存的地址,这种现象怎么可能存在?
所以,这里我们使用的地址绝对不是物理地址!虚拟地址

在这里插入图片描述

进程地址空间:
每个进程都有一个地址空间,都认为自己在独占物理内存
如何管理地址空间?先描述,在组织
先描述:结构体来描述
进程地址空间在内核中是一个数据结构类型,具体进程的地址空间变量

问题:
我们申请1000字节,我们立马能使用这1000个字节吗?
答:不一定,可能会存在暂时不会全部使用甚至是不使用
在os角度,如果空间立马给你,是不是意味着,整个系统会有一部分空间,本来可以给别人立马用的,现在却被你限制着?
操作系统如何衡量你是否在使用这1000字节?
答:有了空间,但是从来没有过读写,

问题:
CPU怎么知道我们的第一行代码在哪里?
答:cpu从一个固定位置开始执行,
地址空间通过页表建立CPU,地址空间,物理内存之间的联系
使得CPU不用寻找第一行代码就可以直接执行代码
即:下面总结为什么要有地址空间第三点
程序的代码和数据可以被加载到物理内存的任意位置,大大的减少内存管理的负担。

问题:OS最终这样设计的目的
达到一个目标,每个进程都认为自己是独占系统资源的

为什么要有地址空间?

  1. 通过添加一层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质目的是为了保护物理内存以及各个进程的数据安全。
  2. 将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存和OS进行内存管理操作,进行软件上面的分离!
  3. 站在CPU和应用层的角度,进程统一可以看作统一使用4GB空间,而且每个空间区域的相对位置是比较确定的!

什么是地址空间?
本质是一种数据类型
实际上是用我们常见的数据结构模拟出来的地址空间
地址空间,页表 在你创建进程的时候就会创建出来
即:创建一个进程包括:描述进程的PCB,进程地址空间,页表,代码和数据

在这里插入图片描述
上面我们的测试用例中,关于这个结果,此时我们对此进行解释:
这里的地址并非是物理地址,而是虚拟地址,为什么不同呢?
根本原因是:
每个进程都有自己的列表
进程是具有独立性的

延伸:
所有的只读的数据,一般可以只有一份:
const char* str1 = “hello”
const chat* str2 = “hello”
打印str1和str2的地址是一样的
即:操作系统维护一份是成本最低的

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值