0


Linux下进程以及相关概念理解

一、进程概念

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

当代码进行编译链接等操作后就会生成一个可执行程序,这个可执行程序本质上也是一个文件,存放在磁盘上。当使这个可执行程序运行起来,本质上是将这个程序加载到内存当中了,因为只有加载到内存后,CPU才能对其进行逐行的语句执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程

并且进程与程序并不一定是对应的,一个程序可以同时运行多次,也就有了多个进程。

竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级

**独立性: **多进程运行,需要独享各种资源,多进程运行期间互不干扰

**并行: **多个进程在多个CPU下分别,同时进行运行,这称之为并行

并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

二、描述进程PCB

我们的电脑上存在着大量的进程,这时就需要操作系统来进行管理。如何管理呢?先描述,再组织

操作系统将每一个进程都进行描述,形成了一个个的进程控制块(PCB,本质上是一个结构体),并将这些PCB以双向链表的形式组织起来。

PCB实际上是对进程控制块的统称,Linux当中的进程控制块为 task_struct,task_struct当中主要包含以下信息:

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

三、查看进程

3.1 通过系统目录查看

在根目录下有一个名为proc的系统目录,该目录中包含大量进程信息。其中有些子目录的目录名为数字,这些数字其实是某一进程的PID,对应文件夹当中记录着对应进程的各种信息。若想查看PID为1的进程的进程信息,则查看名字为1的文件夹即可。

3.2 通过ps命令查看

ps命令的具体使用可以使用man 1 ps命令查看文档

在Linux操作系统下使用ps -l命令会出现下列这种情况。

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

四、进程状态

Linux操作系统的源代码当中对于进程状态有如下定义:

static const char *task_state_array[] = {
    "R (running)",       /*  0*/
    "S (sleeping)",      /*  1*/
    "D (disk sleep)",    /*  2*/
    "T (stopped)",       /*  4*/
    "T (tracing stop)",  /*  8*/
    "Z (zombie)",        /* 16*/
    "X (dead)"           /* 32*/
};

运行状态R

所有处于运行状态的进程(即可被调度的进程),都被放到运行队列当中。当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行。一个进程处于运行状态(running),并不意味着进程一定处于运行当中。运行状态表明一个进程要么在运行中,要么在运行队列里。即可以同时存在多个R状态的进程

睡眠状态S

意味着进程在等待事件完成(该睡眠状态也可称为可中断睡眠

譬如当进程循环向屏幕输出时,由于CPU的处理速度极快,但显示器的速度较慢,导致进程需等待显示器这个资源(CPU此时会处理别的进程)。此时该进程会在运行状态和睡眠状态不断切换,但由于CPU的高速导致我们观测时大概率会看见睡眠状态。

#include <stdio.h>
int main()
{
    while(1){
         printf("handsome boy!\n");                                                                                                                                                                    
    }
    return 0;
}       

显示状态时有个+号表示该进程是前台进程,若没有则是后台进程。

处于该睡眠状态下的进程是可以被杀死的,譬如使用kill命令发送信号

磁盘休眠状态D

一个进程处于磁盘休眠状态,表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动苏醒才可以杀死 。也可称为不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。

譬如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不可被杀掉的。因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。

使用dd命令可以模拟磁盘休眠状态

暂停状态T

在Linux当中,我们可以通过发送SIGSTOP信号使进程进入暂停状态,发送SIGCONT信号可以让处于暂停状态的进程继续运行。

僵尸状态Z

当一个进程将要退出的时候,在系统层面,该进程曾经申请的资源并不是立即被释放,而是要暂时存储一段时间,以供操作系统或是其父进程进行读取,如果退出信息一直未被读取,则相关数据是不会被释放掉的,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。(进程的退出信息存储在该进程的task_struct中)

僵尸状态的存在是必要的,因为进程被创建的目的就是完成某项任务,那么当任务完成的时候,调用方是应该知道任务的完成情况的,所以必须存在僵尸状态,使得调用方得知任务的完成情况,以便进行相应的后续操作。

死亡状态X

死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以几乎不会在任务列表当中看到死亡状态。

五、僵尸进程与孤儿进程

5.1 僵尸进程

5.1.1 僵尸进程的概念

一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。而处于僵尸状态的进程,就是僵尸进程。

如下代码,fork函数创建的子进程在打印5次信息后会退出,而父进程会一直打印信息。即子进程退出了,父进程还在运行,但父进程没有读取子进程的退出信息,那么子进程就进入了僵尸状态。

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

5.1.2 僵尸进程的危害

  1. 若父进程一直不读取进程的退出信息,那么子进程将一直处于僵尸状态。
  2. 僵尸进程的退出信息被保存在task_struct中,若僵尸状态一直不退出,PCB就需一直维护。
  3. 若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费。
  4. 僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,僵尸进程会导致内存泄漏。

5.2 孤儿进程

若父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程。若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收。

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

由于孤儿进程会被init1号进程领养,其并不会造成危害。

六、进程地址空间

6.1 进程地址空间的验证

通过如下代码可以验证进程地址空间与上图一致

#include <stdio.h>                                                                   
#include <stdlib.h>

int un_val;
int init_val = 100;

int main(int argc,char* argv[],char* env[])
{
     int i = 0;
     int count = 0;
     while(env[i] != NULL && count < 5){
         printf("环境变量地址: %p\n",env[i]);
         ++count;
     }
 
     for(int i = 0;i < argc; ++i){
         printf("命令行参数地址: %p\n",argv[i]);
     }
 
     char* p1 = (char*)malloc(10);
     char* p2 = (char*)malloc(10);
     char* p3 = (char*)malloc(10);
 
     printf("栈区地址: %p\n",&p3);
     printf("栈区地址: %p\n",&p2);
     printf("栈区地址: %p\n",&p1);
 
     printf("堆区地址: %p\n",p3);
     printf("堆区地址: %p\n",p2);
     printf("堆区地址: %p\n",p1);
 
     printf("未初始化数据区: %p\n",&un_val);
     printf("初始化数据区: %p\n",&init_val);

     printf("代码区: %p\n",main);
     return 0;
}

栈区向低地址增长,堆区向高地址增长。

6.2 感知进程地址空间

通过下面一份代码我们可以发现一个问题

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>                                                                  

int val = 100;
int main()
{
    pid_t id = fork();
    if(id < 0)//err
    {
        exit(-1);
    }
    else if(id > 0)//father
    {
        sleep(3);
        printf("PID:%d PPID:%d val:%d &val:%p\n",getpid(),getppid(),val,&val);
    }
    else//id == 0
    {
        val = 200;
        printf("PID:%d PPID:%d val:%d &val:%p\n",getpid(),getppid(),val,&val);
    }
    return 0;
}

代码当中用fork函数创建了一个子进程,其中让子进程相将全局变量val该从100改为200后打印,而父进程先休眠3秒钟,然后再打印全局变量的值。按道理来说子进程打印的全局变量的值为200,而父进程是在子进程将全局变量改后再打印的全局变量,那么也应该是200。但是结果并不是,而且两个进程中val变量的地址是一样的,但是为什么打印出的结果不一致呢?

若我们是在同一个物理地址处获取的数据,那一定是相同的,而现在在同一个地址处获取到的值却不同,这只能说明我们打印出来的地址绝对不是物理地址

实际上,我们在语言层面上打印出来的地址都不是物理地址,而是虚拟地址。物理地址用户一概是看不到的,是由操作系统统一进行管理的。所以就算父子进程当中打印出来的全局变量的地址(虚拟地址)相同,但是两个进程当中全局变量的值却是不同的。

6.3 详细认知

进程地址空间地址大小由0x00000000到0xffffffff,且被划分为各个区域,例如代码区、堆区、栈区等。而在结构体mm_struct当中,便记录了各个区域的边界地址。由于虚拟地址是由0x00000000到0xffffffff线性增长的,所以虚拟地址又叫做线性地址

堆向上增长以及栈向下增长实际就是改变mm_struct当中堆和栈的边界地址。

我们生成的可执行程序实际上就被分为了各个区域(例如初始化区、未初始化区等),并且采用与Linux内核中一样的编址方式。当该可执行程序运行起来时,操作系统则将对应的数据加载到对应内存当中即可,大大提高了操作系统的工作效率。而进行可执行程序的"分区"操作的实际上就是编译器,所以说代码的优化级别实际上是编译器说了算。

每个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也会随之被创建。而操作系统可以通过进程的task_struct找到其mm_struct,因为task_struct当中有一个结构体指针存储的是mm_struct的地址。

而当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。体现了进程之间的独立性,这种在需要进行数据修改时才进行拷贝的技术被称为写时拷贝技术

为什么不在创建子进程时完成数据的拷贝呢?

子进程大概率不会使用父进程中所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝。在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间

代码会不会进行写时拷贝?

大多数的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候就需要进行代码的写时拷贝。

为什么要有进程地址空间?

  1. 进程地址空间和页表是OS创建和管理的,凡是非法的访问或映射都会被操作系统终止,起到了保护物理内存中的所有合法数据(各个进程以及内核的相关有效数据),不会存在任何系统级别的越界问题了。
  2. 有了进程地址空间后,每个进程看到的都是相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序等都是相同的,这样一来我们在编写程序的时候就只需关注虚拟地址,而无需关注数据在物理内存当中实际的存储位置。可以让进程以一种统一的视角看待内存,方便以统一的方式来编译和加载所有的可执行程序,简化进程本身的设计与实现。
  3. 有了进程地址空间后,每个进程都认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程调度与内存管理进行解耦。

进程如何创建?

一个进程的创建伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)和页表的创建

七、进程优先级

7.1 优先级概念

优先级实际上就是获取某种资源的先后顺序,而进程优先级实际上就是进程获取CPU资源分配的先后顺序,就是指进程的优先权(priority),优先权高的进程有优先执行的权力。

7.2 优先级存在的原因

优先级存在的主要原因就是资源是有限的,而存在进程优先级的主要原因就是CPU资源是有限的,一个CPU一次只能跑一个进程,而进程是可以有多个的,所以需要存在进程优先级,来确定进程获取CPU资源的先后顺序。

7.3 PRI与NI

  • PRI代表进程的优先级,即进程被CPU执行的先后顺序,该值越小进程的优先级别越高。
  • NI代表的是nice值,其表示进程可被执行的优先级的修正数值。
  • PRI值越小越快被执行,当加入nice值后,将会使得PRI变为:PRI(new) = PRI(old) + NI。
  • 若NI值为负值,那么该进程的PRI将变小,即其优先级会变高。
  • 调整进程优先级,在Linux下,就是调整进程的nice值。
  • NI的取值范围是-20至19,一共40个级别。

注意: 在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI。

7.4 修改进程nice值

7.4.1 top命令

top命令就相当于Windows操作系统中的任务管理器,它能够动态实监测系统中进程资源占用情况

使用top命令后按“r”键,会要求你输入待调整nice值的进程的PID;输入进程PID并回车后,要求输入调整后的nice值。若想退出输入q即可。

7.4.2 renice命令

renice + 更改的nice值 + PID

若想使用renice命令将NI值调为负值,需要root权限

八、环境变量

8.1 概念

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。如编写的C/C++代码,在各个目标文件进行链接的时候,从来不知道所链接的动静态库在哪里,但依然可以链接成功生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。

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

8.2 常见环境变量

  • PATH: 指定命令的搜索路径(系统命令本质也是可执行程序,但启动时不需要指定路径)
  • HOME: 指定用户的主工作目录(即用户登录到Linux系统中的默认所处目录)
  • SHELL: 当前Shell,它的值通常是/bin/bash

8.3 环境变量相关命令

echo:显示某个环境变量的值

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

env: 显示所有环境变量

set:显示本地定义的shell变量和环境变量

unset:清除环境变量

8.4 环境变量的组织方式

在Linux系统当中,环境变量的组织方式如下:

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

8.5 获取环境变量的方法

8.5.1 main函数参数

main函数其实有三个形参,只是平时不常使用所以没有写出来。

int main(int argc, char* argv[],char* env[])
{ …… }

main函数的第二个参数是一个字符指针数组,该数组当中的第一个字符指针存储的是可执行程序的字符串,其余字符指针存储的是所给的若干选项的字符串,最后一个字符指针为空,而main函数的第一个参数代表的就是字符指针数组当中的有效元素个数。main函数的第三个参数接收的实际上就是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量。

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

8.5.2 第三方变量environ

c语言给我们提供了一个全局变量environ,可以利用其访问环境表

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

8.5.3 getenv函数

可以通过系统调用getenv函数来获取环境变量。getenv函数可以根据所给环境变量名,在环境变量表当中进行搜索,并返回一个指向相应值的字符串指针。

#include <stdio.h>
#include <stdlib.h>
int main()
{
    printf("%s\n",getenv("PATH"));                                                                                                                                               
    return 0;
}

标签: linux 服务器

本文转载自: https://blog.csdn.net/GG_Bruse/article/details/128746932
版权归原作者 GG_Bond19 所有, 如有侵权,请联系我们删除。

“Linux下进程以及相关概念理解”的评论:

还没有评论