Linux入门进程概念(超详解,建议收藏)

Posted 世_生

tags:

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


1️⃣进程的基本概念

在给进程下定义之前,我们先了解一下进程:

我们在编写完代码并运行起来时,在我们的磁盘中会形成一个可执行文件,当我们双击这个可执行文件时(程序时),这个程序会加载到内存中,而这个时候我们不能把它叫做程序了,应该叫做进程。

所以说,只要把程序(运行起来)加载到内存中,就称之为进程。

进程的概念:程序的一个执行实例,正在执行的程序等

2️⃣描述进程-PCB

PCB:进程控制块(结构体)
当一个程序加载到内存中,操作系统要为刚刚加载到内存的程序创建一个结构体(PCB),进程信息被放在这个结构体中(PCB),可以理解为PCB是进程的属性的集合。

  • 在Linux操作系统下的PCB是:task_struct
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息

在进程执行时,任意时间内,进程对应的PCB都要以下内容:

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


优先级的理解:
由于CPU只要一个,进程可以有多个,所以CPU的资源有限,操作系统在调度进程到CPU时会根据进程的优先级来判断。
程序计数器的理解:
CPU跑一个进程时,要执行它的代码,而代码是自上往下执行的(if、else、循环等除外),CPU先要取指令,然后分析指令,再然后执行指令。
取完一个指令后,CPU中的寄存器(EIP指令寄存器)会保存当前指令的下一条指令的地址,方便下次取下一个指令。

所谓的函数跳转、分支判断、循环等,都是修改EIP完成的。
上下文数据的理解:
CPU在跑一个进程时,没有跑完就开始切换其他进程,为了下次继续跑完这个进程,会保留这个进程的上下文数据,当这个进程回来时,会把上下文数据移动到CPU内部继续执行。

附加:每个运行的进程都要自己的时间片。
其他的后面会介绍!!

PCB通过双向链表相互连接,操作系统通过PCB,来找到进程。

通过一个例子来快速了解创建进程和删除进程:

🕳小明考到了一个二本学习,今天去学校报到,到了学校(等于程序加载到内存中),学校要通过小明拿走他的档案,并存放好(等于创建进程),当小明毕业了,档案被调出了学校(等于删除进程)。

3️⃣通过系统调用获取进程标示符

编写一段代码:

#include<stdio.h>
#include<unistd.h>
int main()
{
   while(1)
   {
     printf("i am process...pid:%d,ppid:%d\\n",getpid(),getppid());
     sleep(1);
   }
return 0;
 }           

通过ps aux | grep 文件名来找
其中getpid()是找进程的标示符,getppid()找父进程的标示符

4️⃣通过系统调用创建进程-fork(初识)

先编写一段只有一个进程的代码:

#include<stdio.h>
#include<unistd.h>
int main() 
{
   while(1)
   {
     printf("i am process...pid:%d,ppid:%d\\n",getpid(),getppid());                           
     sleep(1);
   }
return 0;
}


只有一个进程在运行。

如何再创建一个进程呢?
——fork函数可以创建一个子进程。

看下面代码及运行结果:

#include<stdio.h>
#include<unistd.h>
int main()
{
  pid_t id=fork();//创建子进程
  while(1)
  {
    if(id==0)
    {
   		printf("i am process...child---pid:%d,ppid:%d\\n",getpid(),getppid());
    	sleep(1);
    }
    else if(id>0)
    {
	     printf("i am process..father---pid:%d,ppid:%d\\n",getpid(),getppid());
	    sleep(1);
    }
    else{
    	;
    }
  }
 return 0;                                                                                  
}

我们可以得出:这段代码有两个进程,并且它们的关系是父子关系。

什么是fork函数:
——在调用fork函数之前,只有一个进程(父进程),当这个进程调用fork函数之后,fork函数会复制一个进程(子进程),区别是PID不同,它们的关系是父子关系。

fork函数会返回两次值:
——给父进程返回子进程的pid。
——给子进程返回0。
——失败时,在父进程中返回-1,不创建子进程,并且errno被适当地设置。

5️⃣进程的状态

操作系统存在着五种状态模型:

  • 新建态:刚刚创建的进程,操作系统还没有把它加入可执行进程组中。
  • 就绪态:进程已经做好准备,只有有机会就会开始执行。
  • 运行态:该进程正在执行。
  • 阻塞态:进程在某些事件发生前不能执行,如I/O操作完成。
  • 退出态:操作系统从可执行进程组中释放出进程,或者自身停止,或者是因为某些原因被取消。


但,我们今天是来看Linux操作系统中具体的进程状态


  • 为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。

下面的状态在kernel源代码里定义

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"T (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
}; 

R 可执行状态

通过命令

ps aux//查看进程的状态


注意此可执行状态®并非上面的运行态。
进程中的R状态不代表正在运行,代表的可被调度,此运行相当于上面的就绪态。

操作系统会把进程中R状态的进程全放在调度队列中,方便调度。

S 睡眠状态

用代码创建一个睡眠状态的进程:

1)创建了一个可执行态进程

#include<stdio.h>
#include<unistd.h>
int main() 
{
    while(1);
	return 0;
}


2)创建休眠态进程

#include<stdio.h>
#include<unistd.h>
int main() 
{
    while(1)
    sleep(10);
	return 0;
}


S状态是浅度睡眠,随时可以被唤醒,也可以被杀掉。

举个例子:
——小明和小华一起去吃饭,而小华对小明说:“小明,你先去楼下等我,我弄会东西,马上就好”。小明下完楼后就开始等小华——着等待的过程就是睡眠状态。

D 磁盘休眠状态

可以表示为深度睡眠,该进程不会被杀掉,即使你是操作系统,除非我自动唤醒,才可以恢复。

举个例子:
——在系统有一个进程叫“小张”,磁盘有一个东西主要进程数据的存储叫“小陈”。
“小张”要把数据存放到磁盘中,拜托“小陈”来存,由于磁盘中的东西较多,“小陈”要找一段时间,而在这个时间段,系统中的正在执行的进程越来越多,最后操作系统看见“小张”“占着茅坑不拉屎”,就把“小张”给踢出去了,之后”小陈“存放数据失败了,找”小张“闻着数据是删掉还是再存放一次,然而”小张“已经被操作系统干掉了,”小陈“得不到回响,不知道怎么办。

为了防止这个情况的发生,操作系统就搞了个D状态。

这种状态(D)的进程杀不死。

T 暂停状态

向进程发送SIGSTOP信号,该进程会响应该信号进入暂停状态,
向该进程发送SIGCONT信号,该进程会从暂停状态恢复到可执行状态。

X 死亡状态 & Z 僵尸状态

僵尸状态:一个处于僵尸状态的进程,会等待它的父进程或操作系统对它的信息进行读取,之后才会被释放。

举个例子:
——一个人突然死亡,普通人不会对现场进行清理,而是报警等警察和法医对该人进行信息的采集,之后才清理现场。

其中,某人充当的角色是进程、警察和法医充当的角色的父进程或者操作系统。

通过代码来模拟僵尸状态的进程

#include<stdio.h>
#include<stdlib.h>                                                            
#include<unistd.h>
  int main()
   {
     	pid_t id=fork();
   		int count=5;
   		while(1)
  		{
     		if(id==0)
    		{
       			while(count){
     			printf("i am process..child---.pid:%d,ppid:%d\\n,count: %d",getpid(),getppi    d(),--count);
    			sleep(1);
   				}
     		 	printf("child quit....\\n");
    		 	exit(1);
   			}
    		else if(id>0)
     		{
     			printf("i am process..father---pid:%d,ppid:%d\\n",getpid(),getppid());
     			sleep(1);
   			}
  		}
 		return 0;
 }

while :; do ps aux |head -1&&ps aux|grep a.out;echo "#######################";sleep 1;done来监控进程的状态。

观察子进程的状态

死亡状态:进程被操作系统释放了或者自己退出了。

6️⃣僵尸进程

当一个进程变为僵尸状态的时候,该进程就变成了僵尸进程。

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。

僵尸进程的危害

如何避免僵尸进程

可以用wait方法和waitpid方法避免,后面文章中讲。

6️⃣孤儿进程

在Linux中,进程的关系主要是父子关系。

一对父子进程中的父进程退出了,子进程还在运行,就会形成孤儿进程。
如果没有进程来回收该子进程的信息,那么会变成僵尸状态,会存在内存泄漏的问题。

为了解决这个问题,该子进程会立即被1号init进程领养。

通过代码来模拟僵尸状态的进程

#include<stdio.h>
#include<stdlib.h>                                                            
#include<unistd.h>
  int main()
   {
     	pid_t id=fork();
   		int count=5;
   		while(1)
  		{
     		if(id==0)
    		{
       			while(1){
     			printf("i am process..child---.pid:%d,ppid:%d\\n",getpid(),getppid());
    			sleep(1);
   				}
     		 	printf("child quit....\\n");
    		 	exit(1);
   			}
    		else if(id>0)
     		{
     			while(count)
     			{
     				printf("i am process..father---pid:%d,ppid:%d\\n",getpid(),getppid());
     				count--;
     				sleep(1);
     			}
     			exit(0);
   			}
  		}
 		return 0;
	 }

while :; do ps axj |head -1&&ps axj|grep a.out;echo "#######################";sleep 1;done来监控进程的状态。

观察子进程的PPID

通过查看该子进程的信息,可以得知该进程被1号init进程领养。

7️⃣进程优先级

基本概念

CPU中的资源是有限的,不可能多个进程一起在CPU上运行,利用优先级把进程有效的先后排好,改善了系统的性能。

  • cpu资源分配的先后顺序,就是指进程的优先权(priority)
  • 优先权高的有优先执行权。

查看系统进程

ps -l可以查看到进程的优先级

PRI:表示这个进程被执行的优先级,其值越小越早执行
NI:表示这个进程的nice值

PIR and NI

nice值表示进程可被执行的优先级的修正值。
PIR=PIR(old)+nice。
当nice为负值时,那么该进程的优先级值会变小,优先级会变高,进程越快被执行。

当然,nice也是有范围的,-20~19,一共40个级别。

查看进程的优先级

用top命令更改已存在的进程的nice

top命令

接着按r然后输入进程的PID输入nice

附加其他概念

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

8️⃣环境变量

基本概念

环境变量(environment variables):一般是指在操作系统中用来指定操作系统运行环境的一些参数

我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但
是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。

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

查看环境变量的方法

echo $name//其中name是你想查找的环境变量名。
常见的环境变量有PATH、HOME、SHELL

查看PATH环境变量的参数

PATH 环境变量

PATH:指定命令的搜索路径

PATH中的参数

在Linux中的一些指令是通过PATH环境变量来查找到指令的路径进而执行的。
ls指令和自己写的代码程序来比较
创建text.c文件

 #include<stdio.h>
 #include<unistd.h>

 int main()
 {
 	while(1)
    {
       printf("hello PATH\\n");
       sleep(1);
    }
 return 0; 
 }


这时因为在使用ls命令时,系统会通过PATH环境变量来查找ls,而a.out没办法通过PATH环境变量来找到,PATH环境变量中没有a.out的路径。

如何可以把自己写的文件可以像ls一样用呢?
把自己文件所在的路径添加到环境变量PATH中
通过export PATH=$PATH:a.out的所在的路径

PATH=$PATH表示把旧的PATH的内容添加到新的PATH内,如何再添加新路径。

同样可以理解,我们咋windows双击软件就可以运行,其实是我们再安装软件时,系统把软件的路径添加到了环境变量PATH中。
所以说,安装软件相当于拷贝到PATH中。

HOME 环境变量

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

在普通用户下:

SHELL 环境变量

SHELL : 当前Shell,它的值通常是/bin/bash。

相关环境变量的相关的命令

echo命令

echo: 显示某个环境变量值
echo $环境变量名

export命令

export: 设置一个新的环境变量
export 环境变量名=$环境变量名:新环境变量

env命令

env: 显示所有环境变量
env

set命令

set: 显示本地定义的shell变量和环境变量
当我们定义一共变量时:
MAXYY=100000

然后通过set查看
set |grep MAXYY

这种变量称之为本地变量
env中找不到,要把本地变量添加到环境变量中去
export MAXYY=100000

unset命令

unset: 清除环境变量
清除上面的MAXYY
通过unset MAXYY清楚MAXYY

环境变量的组织方式

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

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

每个程序中的main函数中都要参数,分别为int arge、char* argv[]、char* envp[]
其中arge表示argv中有效数据的个数,而argv是存放指向命令参数的指针数组,envp是存放指向环境变量的指针数组

用代码演示获取环境变量:

 #include<stdio.h>
 #include<unistd.h>

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


用代码演示argc、argv

 #include<stdio.h>
 #include<unistd.h>
 #include<string.h>
 int main(int argc ,char* argv[],char* envp[])
 {
   if(argc>=2)
   {
      if(strcmp(argv[1],"-a")==0)                                                                                   
      {
    	  printf("hello bit\\n");
      }
      else if(strcmp(argv[1],"-l")==0)
      {
      	 printf("hello Linux\\n");
      }
  
    }
    printf("****************\\n");
    return 0;
   }


ls是一个程序,-a、-l是命令行参数,其ls命令及命令行参数的地址都会存放在在argv中,而argv中的有效数据个数是argc。

结论环境变量的一个系统级别的全局变量,bash之下所以进程都可以获取。

通过系统调用获取或设置环境变量

putenv

putenv:修改或增加环境变量,执行成功返回0,有错误或失败返回-1。

getenv

getenv:用来获取环境变量的内容,执行成功返回指向该内容的指针,找不到符合的环境变量返回NULL。

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

环境变量通常具有全局属性,可以被子进程继承下去。

9️⃣进程地址空间

在我们初学C语言的时候,老师都会给我们画一张空间布局图,并告诉我们全局变量、局部变量、static、空间动态申请和释放、常量在哪一部分内。

可是我们对它并不是很了解。当时我懵懂的以为这就是它们在内存中的分布情况,直到现在才知道这张空间布局图并不是它们在内存上面的分布情况,而是虚拟的。

用代码来验证一下:观察代码中的a_val的值和地址。

用子进程改变a_val的值,但是父进程中的a_val未改变,且地址一样。如果它的地址是指向内存的,那么下面的情况很矛盾。

其实,代码中的a_val的地址的虚拟地址,并不是内存中的地址。每个进程在创建的时候都有自己的进程地址空间,也就是上面介绍的空间布局图。
一个进程的创建会有PCB,PCB中有一个指针,指向自己的进程地址空间(mm_struct)
进程地址空间会通过页表将对应的内容映射到内存中。fork在创建子进程后,由于子进程要修改a_val的值,为了不影响父进程,操作系统就会重新在内存中开辟一块4字节的空间来存放修改后的a_val,然后子进程的页表将会修改掉进程地址空间通过页表映射到内存中的a_val的地址。

如何理解虚拟地址

我读小学的时候,经常和同桌闹矛盾,划三八线,谁超线了就要挨一下打,不许还手,现在想想很幼稚哈哈。

为了更加公平的划分张三、李四的范围,我们可以定义一个结构体。

//数据化
struct destop{
unsinged long start_zhangsan;
unsinged long end_zhangsan;

unsinged long start_lisi;
unsinged long end_lisi;
}

虚拟地址就像上面的刻度一样。

所以,进程地址空间本质是一种内核的数据结构(mm_struct)。

在32位下,系统访问的内存4GB,也就是232的字节。
程序员访问的是232空间大小和物理内存本身是什么关系?
232空间大小相当于刻度尺,物理内存本身相当于桌子。

//区域划分
struct mm_struct{
unsinged int code_start;
unsinged int code_end;
unsinged int readonly_start;
unsinged int readonly_end;
unsinged int init_start;
unsinged int uninit_end;
unsinged int stack_start;
unsinged int stack_end;
//………………
}

可执行程序在磁盘中保存,.exe已经在划分好了
可执行程序是分段的。

为什么要有进程地址空间???
1、从此以后不会有任何系统级别的越界问题存在了

如果没有进程地址空间,一个程序访问一个野指针并写入时,这会直接改掉物理内存中的数据。
如果有进程地址空间,程序访问野指针时,会先通过页表,页表发现你从来没有这种映射,页表就会终止访问。

虚拟空间+页表:本质是保存内存。

2、每个进程都有自己的进程地址空间,并且都要相同的空间范围(构成,顺序)。
3、可以更好的完成进程独立性及合理使用空间。

📖总结

这次主要的介绍了一些进程的概念,初识fork,进程的状态,以及僵尸进程和孤儿进程,还有环境变量以及纠正以前的错误认知了解了进程地址空间。
上面有错误的地方希望指出,共同学习。

以上是关于Linux入门进程概念(超详解,建议收藏)的主要内容,如果未能解决你的问题,请参考以下文章

linux shell 脚本 入门到实战详解[⭐建议收藏!!⭐]

linux shell 脚本 入门到实战详解[⭐建议收藏!!⭐]

linux shell 脚本 入门到实战详解[⭐建议收藏!!⭐]

超详解干货建议收藏正则表达式 & re模块

Linux——超超讲解SSH的原理与SSH的实现!建议收藏❤

❤️数据结构和算法动图演示,超详细,就怕你不会!二分查找详解建议收藏❤️