linux进程

什么是进程

windows的进程管理器中,可以看到所有运行的进程。 启动一个软件,本质就是启动了一个进程。 在linux中,运行一条命令,./xxx时,就是在系统层面创建了一个进程。

linux是可以同时加载多个程序的,Linux系统可能同时存在大量的进程在内存中!!!!

ps axj:查看系统中所有进程

linux要把这些进程管理起来 ------ 先描述,再组织!!!

相当多的程序都被加载到内存中,cpu要调度某一个加载到内存中的程序 ------ 哪一个执行完、哪一个应该被删除、哪一个优先级较高等,这些需要os管理起来

linux为每一个进程都创建一个pcb结构体,保存了该进程所有的属性 ------ task_struct

进程 = 对应的代码和数据 + os内核为了管理进程而创建的数据结构

例如:如果一个人是学校的学生,他要在学校,同时还要被学校管理起来。

如果人在学校就是学生,那么保安也是、图书馆管理员也是。所以除了要有对应的人,

个人的属性信息、籍贯、宿舍、入学时间等,都应该录入到学校的教学管理系统中。

对学生的增删查改就是对教学管理系统的增删查改,对进程的增删查改就变成了对进程相关数据结构的增删查改

PCB是什么,内部有什么属性?

pcb(process control block):进程控制块,在linux下叫 task_struct,用来描述一个进程的属性信息

以下是部分属性:

1 标识符:描述进程的唯一标识符,用来区别其他进程 PID,和学号一样

2 进程状态:标识进程正在运行,阻塞等

3 优先级:确定资源谁前谁后使用(排队打饭)

4 程序计数器:进程中即将被执行的下一条指令的地址

CPU怎么知道进程运行到哪一行代码?进程pcb里和cpu内都有个pc指针,保存了当前进程下一条指令地址。

5 内存指针:指向进程对应的代码和数据

6 上下文数据:进程被调度时,cpu寄存器中的数据

7 I/O状态信息:当前进程能使用哪些io设备,例如------该进程打开的文件列表

8 记账信息:累记被cpu处理了多长时间等

9 当前进程的所在工作目录cwd:进程在哪个路径被创建的

系统中查看进程

ps axj ------ 查看系统中所有进程

top ------ 相当于windows的任务管理器

ls /proc ------动态 我们当前运行的所有进程 都会以文件的形式 展示在/proc,多一个进程就会多一个目录,少一个进程就会少一个目录

例如:在我的工作目录运行死循环进程,查看该进程的属性信息

进程的父子关系

进程有父子关系

scss 复制代码
pid_t getpid()//获取当前进程pid
pid_t getppid() //获取父进程pid

在命令行解释器中,后台运行多个死循环进程,会发现它们的父进程都是一样的,父进程就是bash(shell外壳程序)

scss 复制代码
while(1){}          

shell运行原理:在同一个终端,命令行下,父进程bash创建子进程去执行任务

关了bash,当前的终端就不能正常的进行命令行解释

另外,每一次登录,都会有新的bash

fork创建子进程

fork基本用法

pid_t fork()

1 作用:fork()以后,会有2个执行流

2 返回值

创建失败: 返回-1

创建成功: 给父进程 返回 子进程的pid;给子进程返回0

创建出来,是为了做不一样的事情,把任务拆解成让不同的执行流去执行,才能提高效率 ------ 让父子进程执行不同代码 ,所以用条件判断做不同的事情

在父进程里 id=子进程的pid,在子进程里 id=0

scss 复制代码
int main()                                                                                                                                                           
{    
    pid_t id = fork();    
    if(id < 0)    
    {    
        //创建失败    
        perror("fork error");    
        return 1;    
    }    
    else if(id == 0)    
    {    
        //child process(task)    
        //父进程能看到,但他不能执行    
        while(1)    
        {    
            sleep(1);    
            printf("i am child, pid:%d,ppid:%d\n",getpid(),getppid());    
        }    
    
    }    
    else if(id > 0)    
    {    
        //parent process    
        while(1)    
        {    
            sleep(1);    
            printf("i am parent,pid:%d,ppid:%d\n",getpid(),getppid());    
        }    
    }    
    return 0;    
}    

为什么给子进程返回0,给父进程返回子进程的pid(用来记忆)

父进程:子进程 = 1:n

1个父亲,可能有多个小孩;1个小孩只有一个父亲

父亲给孩子取名字来标识一个小孩,fork之后,给父进程返回子进程的pid

子进程只有1个父进程,可以很方便找到 ------ getppid

fork调用失败原因

1 系统有太多进程,进程创建是要消耗内存资源的

2 当前用户的进程数量超过限制

进程状态

维护进程状态,本质是在更改task_struct里的status属性,用不同的数字标识

arduino 复制代码
#define NEW 1
#define RUN 2

struct task_struct{
    int status;
}

1 新建:初步具备了进程相关数据结构。字面意思(但linux内核中没有这个状态)

2 运行:task_struct结构体在运行队列中排队,就叫做运行态 !!!即等待cpu资源的队列

3 阻塞:系统中有各种资源,不仅仅是cpu(运行队列),还有网卡、文件、磁盘等其他设备

系统中不仅存在一种队列,还有等待其他资源的队列。

例如:等待磁盘资源就绪的队列

进程等待非cpu资源就绪时,要放到其他队列中,这种队列就是阻塞队列。

即放在阻塞队列中等待非cpu资源就绪的进程,就处于阻塞状态

arduino 复制代码
int main(){
    int a;
    scanf("%d",&a);//等待键盘数据就绪 ./xxx.exe -> 运行变成进程,如果一直不输入,进程就卡在那里
    //阻塞状态
    return 0;
}

4 挂起:当内存不足时,os把进程的代码和数据适当换出到磁盘,进程状态就是挂起!!!!

例如:一个大型游戏128GB,os不可能把游戏进程的所有代码和数据放进内存。刚开始进主菜单时,只把游戏的主菜单代码和数据换入到内存执行;当进入游戏界面后,主菜单的代码和数据可以直接换出,当需要时再换入进内存

5 挂起阻塞:进程正在等待某种非cpu资源,进程pcb被放到阻塞队列,同时它的代码和数据被置换到swap分区

linux具体进程状态

1 TASK_RUNNING对应上面的运行态,标识进程正在运行队列或者正在被cpu调度

csharp 复制代码
int main(){
    while(1){}
}

2 interruptible ------ 可中断睡眠,就是等待非cpu资源,对应上面的阻塞状态,可以用信号中断

arduino 复制代码
int main(){
    int a;
    while(1){
    scanf("%d",&a);//等待外设资源就绪
    }
}

+号意味着这个任务属于前台进程------启动会占用命令行对话框(不能执行输入的命令),可以用ctrl+c终止

后台进程:运行进程时,后面带& ------ 不影响命令行交互

3 uninterruptible ------ 不可中断睡眠,不可被信号中断

理解:假设进程想把某一块数据,刷新到磁盘中。

进程A把数据交给磁盘,磁盘就在自己内部找对应位置写数据,磁盘是机械设备,很慢;

磁盘是否写成功,要告诉给进程A ------ 进程A要等磁盘结果,等待某种资源就绪,进程状态是阻塞状态。

正好在进程A等待期间,os收到了很多任务,有很多进程,os的内存非常紧张。

os看到进程A在休眠,等待磁盘资源就绪,os就可能把这个进程杀掉!!!!

服务器压力过大时,os会通过一定手段,杀掉一些进程来节省空间的作用

此时磁盘数据写失败了,想发结果给对应进程,可是进程已经被干掉了 ------ 数据丢失!!!

如果进程正在磁盘读写有关的,进程在阻塞,同时在阻塞期间,不允许os杀掉,就设为uninterruptible

只能等这个进程得到磁盘读写结果,读写成功或失败后,进程才能被唤醒.

4 stopped ------ 用信号kill -19 pid,向目标进程发送19号信号SIGSTOP,让进程停止执行了。

5 traced ------ 代码正在调试

6 exit_dead终止 ------ os面对许多个进程pcb,同一时间可能有大量的进程退出,系统释放资源得一个个来。

X状态一定要维持起来,让os能识别到可被回收,但os可能正忙着回收其他的进程或其他,所以这个状态要被维护

7 exit_zombie僵尸状态 ------ 进程退出,但是进程的资源还没有被父进程回收,例如运行结果

僵尸进程

一个进程已经退出,但是还不允许os释放,处于一个被检测的状态,叫做僵尸状态!!!!!!!

一个进程是否正常退出、允许结果等,谁来关心呢?一般父进程或者os(父进程也挂了的情况下)

fork()创建子进程,为什么要创建子进程?因为要让他可以执行不一样的代码,完成别的事情。那子进程退出了,父进程想不想知道子进程把事情做得怎么样??当然,也可能不关心。

维持该状态,是为了让父进程或os回收子进程的退出结果(子进程办的怎么样,异常还是正常退出)

scss 复制代码
int main()    
{    
    pid_t id = fork();    
    if(id == 0)    
    {    
        //子进程先退出        
    }    
    else if(id > 0)    
    {    
        while(1){}                                                           
    }                                                               
    else if(id < 0)                                                 
    {                                                               
        perror("fork");                                             
        exit(1);                                                    
    }                                                               
    return 0;    
}    

一个进程处于僵尸状态,数据结构pcb没有回收,代码和数据可以被释放,

但它的数据结构会占据内存 ------ 内存泄漏

孤儿进程

父进程如果提前退出,子进程后退出,进入Z状态后,如何处理?

父进程先退出,子进程还在,子进程就称为孤儿进程!!!

孤儿进程被1号Init进程领养,当然由init进程回收

scss 复制代码
int main()    
{    
    pid_t id = fork();    
    if(id == 0)    
    {    
       while(1){}     
    }    
    else if(id > 0)    
    {    
        //父进程先退出                                                                                 
    }                          
    else if(id < 0)            
    {                                                            
        perror("fork");                                          
        exit(1);                                                 
    }                                                            
    return 0;                                                    
}               

进程优先级

为什么要有优先级?

因为cpu是有限的,但进程太多。需要通过某种方式竞争资源

什么是优先级

确认是谁先获得某种资源,谁后获得。

我们是可以用一些数据来表明优先级.

优先级是调度器调度的主要参考

调度:把谁先从运行队列中放到cpu上,把谁将cpu上拿下来

linux具体的优先级做法

ps -al 查看当前会话中相关进程 PRI:代表这个进程可被执行的优先级,其值越小,优先级越高,越早被执行priority

NI:NICE值,优先级的修正数值

优先级 = 老优先级 + nice值

默认每一次设置优先级,老的优先级都是80

这样NICE为负值时,优先级就会提高,越快被执行

所以,调整优先级,在linux下就是调整NICE值

如何修改进程的优先级

1 top -> 输入r -> 输入进程pid -> 要设置的NICE值

2 nice值的取值范围[-20,19]

3 由于nice的取值范围,以及老的优先级默认都是80,linux下创建的进程优先级范围[60, 99]

4 如果允许优先级调整范围过大,就可能有些恶意进程恶意调整自己的优先级,导致cpu调度器总先调度 它,导致其他进程长时间得不到调度。

重要概念

竞争性

进程数目众多,cpu资源有限,需要竞争资源,因此进程之间有竞争性

独立性

进程之间相互独立,互不影响。

例如:画图板崩了,不会影响xshell客户端。

例如:父进程挂了,不会影响子进程。

并行

电脑上有多个cpu

在任何一个时刻,同时有2个以上的进程在被调度运行 ------ 并行

并发

什么是并发?

大部分情况下,电脑只有一个cpu

os在进行调度时,在运行队列中选择特定的进程,放到cpu上去运行

任何一个时刻都只有一个进程在跑

但在一个时间段内,基于时间片、基于切换、基于抢占和出让

多个进程的代码都得以推进,这就是并发

并发如何实现的?

1 时间片:

给每个进程相对固定的时间,让每个进程都要在特定的时间段内,较为均衡占用cpu资源

如果时间片没到,进程就执行完了,进程会立刻从cpu下来;

例如:A进程是个死循环,但它在cpu上占用10ms后必须下来,os选择下一个进程继续运行

再比如:A进程放到cpu上,不可能一直占有cpu资源,例如只给10ms,10ms后不管运行了多少,都必须从cpu上下来。

2 抢占和出让:

抢占 ------ 优先级较高的进程抢占正在cpu上跑的进程

出让 ------ 把自己从cpu上拿下来,放到其他队列中

3 切换:

如果进程A正在被运行,cpu的寄存器里,一定保存的是进程A的临时数据

这些临时数据就是进程A的上下文数据!!!!!!!!

如果丢弃,那下次进程A再次被调度时,就不知道运行到哪里,以及函数的返回值都会丢失

进程运行期间,被抢占了、时间片到了、主动出让了,导致进程B被切换上去,当进程A被切走时,进程A要带走自己的上下文数据 ------ 带走保存的目的:下一次被调度时,能恢复上去,就能继续按照之前的逻辑继续向后运行

环境变量

os本身,也可以自己定义一些环境变量,这些变量通常是与系统、用户相关的一些系统参数。

PATH环境变量

系统命令可以直接运行,我自己的程序必须带路径才能运行

系统要运行一条命令,就要先找到它。系统命令是可被找到的,因为这些命令的路径在环境变量PATH中

PATH里保存了大量的路径,如果运行程序时不指定路径,就会去这些搜索路径中找

例如:执行ls时,系统先在PATH里的地址中查找ls.

添加路径到PATH(在命令行上改环境变量,只能在本次登录中被修改,退出后会重置)

把可执行程序拷贝到PATH维护的路径中,这就是安装程序。

常见环境变量

PATH:指定可执行程序的搜索路径(不手动指定可执行程序的路径时,就会到这些路径里去找)

HOME:指定当前用户的主工作目录(/home/xxx),root是/root,不同用户的HOME是不同的

SHELL:当前Shell,为/bin/bash

每个环境变量保存的内容没有关联,查看linux下所有的环境变量 ------ env命令

代码中获取环境变量

1 main函数获取

arduino 复制代码
int main(int argc,char* argv[],char* env[])    
{    
      for(int i=0;env[i]!= NULL;++i)    
      {    
          printf("%s\n",env[i]);    
      }                                                                                                                                                              
      return 0;    
}    

main函数的形参都会有一个环境变量表,是一个字符指针数组,每个指针指向一个字符串

2 unistd.h中定义了一个全局变量environ,和main函数里的char* env[]没有区别

用man手册,在命令行中输入man environ就可以找到:

3 获取某一个环境变量 ------char* getenv(const char* name)

perl 复制代码
 printf("%s\n",getenv("PATH"));

进程环境变量来源

子进程的环境变量是从父进程那继承的!!!

默认,所有的环境变量都会在父进程继承

arduino 复制代码
int main(int argc,char* argv[],char* env[])    
{    
     printf("%s\n",getenv("aaaaaa"));                                                         
     return 0;    
}

bash的环境变量从哪里来呢?

linux的配置文件,当登录的时候会自动执行,使用特定的命令给bash装载特定的环境变量

环境变量的全局属性

环境变量具有全局属性 ------ 可以被所有子进程继承

命令行也可以定义普通变量,但不能被子进程拿到

进程地址空间

排布

c/c++定义的所有变量,都要遵守以下规则:

栈区向地址减小的方向增长,堆栈相对而生

static修饰局部变量,本质将该变量,开辟在全局区域

静态变量存在全局区,字面常量在代码段

c 复制代码
  int un_g_val;    
  int g_val = 1;    
  int main(int argc,char* argv[],char* env[])    
  {    
      int a = 0;    
      int b = 0;
      static int c = 1;
      char* heap = (char*)malloc(sizeof(char));
      char* heap1 =(char*)malloc(sizeof(char));
      char* heap2 =(char*)malloc(sizeof(char));
      printf("env:%p\n",env[0]);    
      printf("argv:%p\n",argv[0]);    
      printf("stack:---\n");    
      printf("&a=%p\n",&a);    
      printf("&b=%p\n",&b);    
      printf("&heap=%p\n",&heap);    
    
      printf("heap:%p\n",heap);
      printf("heap1:%p\n",heap1);
      printf("heap2:%p\n",heap2);
      printf("un_g_val:%p\n",&un_g_val);    
      printf("g_val:%p\n",&g_val); 
      printf("&c(static):%p\n",&c);
      printf("main:%p\n",&main);
      printf("read only string addr:%p","aaa");
      
      return 0;    
  }                 

这里的地址空间是内存吗

不是内存,甚至指针,都不是真正的内存地址!!!

perl 复制代码
  int g_val = 1;    
  int main(int argc,char* argv[],char* env[])    
  {    
      pid_t id = fork();    
      if(id == 0)    
      {    
          printf("child:g_val->10\n");    
          g_val = 10;                                                                                                                                                
          while(1)                                                                                 
          {                                                                                        
              sleep(1);                                                                            
              //子进程                                                                             
              printf("child:g_val = %d,&g_val=%p\n",g_val,&g_val);                                 
          }                                                                                        
      }                                                                                            
      else                                                                                         
      {                                                                                            
          while(1)                                                                                 
          {                                                                                        
              //父进程                                                                            
              sleep(1);                                                                            
              printf("parent:g_val = %d,&g_val=%p\n",g_val,&g_val);                                
          }                                                                                        
      }                                                                                            
      return 0;                                                                                    
  }                           

同一个地址,不同的进程读取时,出现了不同的值

这里的地址,绝对不是物理内存的地址!!!

如何理解进程地址空间

1 历史:直接访问物理内存

进程1、进程2、进程3在内存中运行,一旦发生野指针问题,例如进程1里非法访问/篡改了进程2的数据,

这会导致进程之间不具有独立性,且不安全

内存本身是可以随时被读写的,没有权限、不可读等,它就是个硬件,只负责存和取

所以我们不能让用户直接使用物理地址

2 现代计算机,提出了下面方式

用户不直接使用物理内存

linux给每一个进程创建一个地址空间(虚拟地址空间),这个空间上所有的地址,叫虚拟地址。

进程控制块可以找到虚拟地址空间,上面所用到的地址都是虚拟地址。

系统也一定存在一种映射机制,核心工作:把用户的代码通过该映射机制映射到物理内存中

凡是要访问物理内存,需要先进行映射!!!!!!!!!

(1) cpu拿到进程pcb中指向的代码和数据(或者上下文数据),cpu读到了某个虚拟地址

(2) 根据虚拟地址,经过映射,访问物理内存中的代码和数据

虚拟地址空间和映射机制的存在,可以在软件层上甄别为正常访问还是非法访问

如果是非法访问就禁止映射,变相保护了物理内存

进程地址空间的本质

每个进程都有进程地址空间,因此系统中会有许多进程地址空间,需要被管理起来

先描述,再组织

内核中的地址空间,本质也一定是一种结构体

同时,将来也一定要和特定的进程pcb关联起来!!!!

在进程的task_struct里,有一个指向进程地址空间的指针

区域划分

针对一个特定的区域,定义上start和end.

区域划分,本质是在一个范围里定义出start和end

地址空间是一种内核数据结构,它里面至少要有:各个区域的划分

arduino 复制代码
struct mm_struct
{
    int code_start;
    int code_end;
    
    int init_start;
    int init_end;
    
    int uninit_start;
    int uninit_end;
    
    int heap_start;
    int heap_end;
    
    ..............................
}

范围变化本质就是对start或end标记值 +-特定的范围即可

内核中是用无符号整型来表示各个区域的

页表

由操作系统维护(os内的一种内核数据结构),用来虚拟地址到物理地址之间的映射

地址空间和页表,是每个进程都私有一份

只要保证每个进程的页表映射的是物理内存的不同区域,就能做到进程之间不会互相干扰,保证进程的独立性

fork之后,为什么一个变量保存两个不同的值

在创建子进程后,父子进程的g_val相同,页表映射到同一位置

当子进程改了g_val值时,os在内存的其他地方拷贝一份g_val,让子进程的页表映射到这个位置,再进行修改,虚拟地址的值不受影响,但通过页表映射到不同位置

父子进程各自在物理内存中,有属于自己的变量空间!!

只是在用户层用同一个变量(虚拟地址)来标识了。

编译器对虚拟地址空间的支持

可执行程序在磁盘上,内部已经有虚拟地址了

objdump是用来显示二进制文件信息的工具

objdump -h 可执行程序:可以看到VMA(Virtual Memory Address)

h选项:显示文件的节信息,可执行程序是由节构成的,每个节包含特定代码和数据。

例如:.text存放可执行程序的代码(所有函数和指令),.data存放初始化的全局变量和静态变量

地址空间不仅是os内部要遵守的编译器也要遵守!!!编译器编译代码时,就已经给我们形成了各个区域的代码区、数据区......并且采用和linux内核一样的编址方式,给每一个变量,每一行代码都进行了编址。

即:程序在编译时每一个变量/函数/指令早已具有了一个虚拟地址。 加载程序时,其实虚拟地址也被加载进去了

ini 复制代码
    int a = 10;  //0x1
    int b = 100; //0x2
    int c = 1000;//0x3

进程内部的地址,用的是编译器编译好的虚拟地址

当程序加载到内存时,每行代码、每个变量才具有了一个物理地址

1 地址空间可以按照加载进来的程序代码和数据,来限定地址空间的区域划分,填充mm_struct

2 把虚拟地址填写到页表左侧,再把实际代码和数据的物理地址,填写到页表右侧,此时就构建好了映射关系。

3 cpu读取时,先根据进程pcb获取进程下一行代码或数据的虚拟地址,再根据页表,从内存中拿到代码和数据

4 当cpu读到指令时,指令内部也有地址,指令内部的地址,还是虚拟地址

cpu至始至终都在用虚拟地址,通过页表映射拿到内存中的值

为什么要存在地址空间和页表

1 因为有地址空间和页表的存在,可以对用户的非法访问进行有效拦截

凡是非法的访问或映射,os都会识别到,并终止你这个进程

ini 复制代码
char* str = "dadwadaw";
str[0] = 'A';//只读

2 实现了内存管理和进程管理的解耦

地址空间的排布是严格的,而页表可以帮我们映射到物理内存的任何位置,所以在物理内存中,可以对未来的数据进行任意位置的加载,只要保证页表映射即可

3 在分配内存时,可以采用延迟分配的策略,提高内存的有效使用率

如果我申请了空间,不立刻使用,就会造成空间的浪费。因为有地址空间的存在,上层申请空间,其实是在地址空间上申请的,物理内存可以甚至1byte都不给你!!!

当你真正对物理地址空间访问时(os自动完成,用户包括进程零感知),才执行内存的相关管理算法,帮你申请内存,构建页表映射关系,然后,再让你进行内存的访问。

4 在进程视角(用户视角),所有的内存分布,都可以是有序的,方便确定越界情况

5 进程的独立性,可以通过地址空间+页表的方式实现

因为有地址空间的存在,这些虚拟地址可以通过页表映射到不同的区域,来实现进程的独立性

拓展

实际上各自外设,例如磁盘、显卡等,都是有各种寄存器的.

把内存的存储空间,和外设的寄存器,统一编址,当作内存来看,这样计算机在写数据时,写入外设就好像直接写到内存里,只有时间上的差别。但是每一种硬件本身是不同的,所以要引入一个虚拟地址空间。

虚拟地址空间统一将不同硬件对应的设备(内存、网卡、显卡、磁盘等)进行编址,从0000......00到fff......fff,我们访问某些硬件时,特定的硬件和地址空间有对应的关系。所以在访问某个区域时,如果对应的是内存就访问内存,如果外设就访问外设,

在进程这一端看来,它是以统一的视角去看待所有设备

写时拷贝

引出

fork创建子进程,os做了什么?

系统里多了一个进程:进程 = 内核数据结构+进程代码和数据

1 为子进程创建内核数据结构

2 将父进程部分数据结构内容拷贝给子进程,例如上下文数据、进程地址空间的区域划分、页表的映射

3 添加子进程到运行队列中

但是,子进程也要有自己的代码和数据。一般而言,子进程没有自己的代码和数据!

所以子进程只能使用 父进程的代码和数据

代码:只读的,父子共享,没有问题

之前的代码,子进程也能看到,但子进程不会执行

因为进程随时可能被中断(可能并没有执行完),下次回来,还必须从之前的位置继续运行(不是最开始),就要求cpu必须随时记录下,当前进程执行的位置.所以cpu内有对应的寄存器数据,用来记录当前进程的执行位置EIP-pc指针

fork创建子进程,子进程会认为自己的EIP起始值,是fork之后的代码位置

数据:可能被修改,写时拷贝

是什么

本质是一种延迟申请,修改/写入的时候再申请内存,拷贝数据。

用的时候再分配,是高效使用内存的一种表现

作用

父子进程得以彻底分离,代码共享,数据写时拷贝,保证进程独立性

进程终止

进程终止时,os做了什么?

进程 = 内核数据结构 + 进程对应的代码和数据

释放进程申请的相关数据结构,以及对应代码和数据

进程终止的情况

a 代码跑完,结果正确

b 代码跑完,结果不正确

c 代码没有跑完,进程异常终止,此时结果不重要了

(1) main函数的返回值?意义是什么?

是进程的退出码,用来表示进程的结果是否正确

0:代码跑完,结果success

非0:代码跑完,结果不正确,可能有很多错误原因

csharp 复制代码
int main(){
    return 2;//返回给父进程,用来评判该进程执行结果用的,可以忽略
}

在命令行中,获取最近一个进程执行完毕之后,它的退出码 ------ echo $?

echo $?本身也是一个进程

(2) char* strerror(int errnum);//将退出码转换成字符串描述

查看常见的错误原因(系统默认给的退出码都代表什么错误原因)

perl 复制代码
      for(int i = 0; i < 128; ++i)    
      {    
          printf("%d: %s\n",i,strerror(i));    
      }    

(3) 也能自己设计一套退出方案,方便定位错误

(4) 代码没跑完,程序崩溃,退出码无意义。例如:野指针问题、除0错误

在代码中终止进程的方式

(1) main函数里的return语句

(2) 代码的任意位置终止进程:

void exit(int status); // c语言提供的库函数 stdlib.h

void _exit(int status);//系统调用接口

区别:exit会刷新缓冲区到os,_exit不会

scss 复制代码
     printf("you can see me");
     //数据在缓冲区内
     exit(0);//会将缓冲区内的数据全部刷新出来
     //_exit(0);//不会刷新缓冲区的数据                                                                                                    

exit底层调用了_exit,只是在调用之前,要刷新缓冲区到os内

调用printf等io函数,数据要保存到缓冲区,这个缓冲区,是c标准库函数维护的

进程等待

为什么要进行进程等待

子进程退出,父进程不管子进程,子进程就要处于僵尸状态 ------ 导致内存泄漏

父进程创建了子进程,是要子进程办事的,那么子进程把任务完成得怎么样?父进程需要关心吗?如果需要,如何得知?如果不需要,该如何处理?

核心任务:回收子进程相关内存资源;父进程通过等待,获得子进程退出结果

进程等待基本使用

pid_t wait(int* status)

成功返回子进程pid,失败返回-1 ------ 阻塞式等待任意子进程退出

scss 复制代码
pid_t id = fork();
  if (id < 0)
  {
    perror("fork");
    exit(1);
  }
  else if (id == 0)
  {
    // 子进程
    sleep(5);
    exit(0);
  }
  else
  {
    // 父进程
    pid_t ret = wait(NULL);//wait是阻塞式等待,等待子进程退出
    if(ret > 0)
    {
      printf("等待子进程成功,ret=%d\n",ret);
    }
  }
  return 0;

pid_t waitpid(pid_t pid, int* status,int options);

(1) pid_t pid:指定要等哪一个子进程 ------ 一个父进程可能有多个子进程

pid = -1:等待任意一个子进程退出,与wait等效

(2) int options:0表示阻塞等待;WNOHANG表示非阻塞等待

(3) int* status:输出型参数,获得子进程得退出结果,NULL表示不关系结果

arduino 复制代码
pid_t ret = waitpid(-1,NULL,0);//等价于wait(NULL)

status的构成

子进程的退出结果有3种:

(1) 代码跑完,结果正确

(2) 代码跑完,结果不正确

(3) 代码没有跑完,进程崩溃了 ------ 例如野指针越界访问、除0错误

本质是os通过发送信号,杀掉了这个进程

status按照比特位的方式,将32个比特位进行划分 ------ 只关注低16位

0x7f -> 0111 1111,子进程收到的信号 = status & 0x7f,如果为0表示正常退出

0xff -> 1111 1111,子进程的退出码 = (status >> 8) & 0xff

系统提供宏不需要手动位操作

WIFEXITED(status): 查看进程是否正常退出

WEXITSTATUS(status): 查看进程的退出码

scss 复制代码
pid_t id = fork();
  if (id == 0)
  {
    // 子进程
    sleep(5);
    exit(99);
  }
  else if(id > 0)
  {
    // 父进程
    int status = 0;
    pid_t ret = waitpid(-1,&status,0);//等价wait(NULL)
    if(ret > 0)
    {
      printf("等待子进程成功,子进程pid=%d,子进程收到的信号signal=%d,退出码exitCode=%d\n",ret,status&0x7f,(status>>8)&0xFF);
      if(WIFEXITED(status) == 1)
      {
        //子进程正常退出
        printf("子进程正常退出,子进程的退出码是%d\n",WEXITSTATUS(status));
      }
    }
  }

wait/waitpid原理

进程具有独立性,进程退出码,不也是子进程的数据吗?父进程怎么通过wait/waitpid拿到???

僵尸进程:至少要保留进程的pcb,task_struct里面保留了任何进程退出时的退出结果,让父进程调用wait/waitpid读取。

wait/waitpid本质是在读取子进程的task_struct结构体里的退出码和退出信号,将它们放进status里

waitpid伪代码实现

ini 复制代码
waitpid(child_id,status,flag){
    //os内核实现
   //检测子进程退出状态,查看子进程的task_struct里的内核status
   if(内核status == 退出){ 
       //读取子进程task_struct里的退出结果,放入*status里
       *status = 0;
       *status |= sig_number;
       *status |= (exit_code << 8);
       //返回子进程pid
       return child_pid;
   }
   else if(内核status == 没退出)
   {
       if(flag == 0)
       {
           //把父进程pcb放到等待队列中,等待子进程退出
           阻塞;//进程阻塞,本质是进程阻塞在系统函数的内部!!!!
           //当条件满足时,父进程被唤醒,继续往后执行
           
           //读取子进程task_struct里的退出结果,放入*status里
           *status = 0;
           *status |= sig_number;
           *status |= (exit_code << 8);
           //返回子进程pid
           return child_pid;
       }
       else if(flag == WNOHANG)
       {
            //不阻塞进程
            return 0;
       }
   }
   else{//出错了 return -1;}
}

基于非阻塞调用的轮询检测方案

父进程通过调用waitpid来等待,如果父进程发现子进程没有退出,waitpid直接返回,不会阻塞

scss 复制代码
typedef void (*callback)();//函数指针类型
std::vector<callback> callbacks;
void task1()
{
      printf("任务1\n");
}
void task2()
{
      printf("任务2\n");
}
//想要父进程闲的时候执行的方法
void load()
{
      callbacks.push_back(task1);
      callbacks.push_back(task2);
}

int main()
{
      pid_t id = fork();
      if (id == 0)
      {
            // 子进程
            sleep(3);
            exit(99);
      }
      else if(id > 0)
      {
            //父进程
            int status = 0;
            int quit = 0;
            while(!quit)
            {
                  pid_t ret = waitpid(id,&status,WNOHANG);
                  if(ret > 0){
                        //等待成功,子进程退出
                        quit = 1;
                        if(WIFEXITED(status) == 1){
                          printf("子进程pid=%d,子进程退出码exitCode=%d\n",ret,WEXITSTATUS(status));
                        }
                        else{
                          printf("子进程收到信号异常退出:%d\n",status&0x7f);
                        }
                  }
                  else if(ret == 0)
                  {
                        //子进程还没有退出,再干会别的事情
                        printf("子进程还在运行\n");
                        if(callbacks.empty() == true)
                          load();
                        for(auto func:callbacks)
                        {
                          func();
                        }
                        sleep(1);
                  }
            }
      }
      return 0;
}

进程程序替换

是什么

fork之后,父子各自执行父进程代码的一部分,但如果子进程想执行一个全新的程序呢?

进程程序替换通过特定接口,加载磁盘上的全新程序(代码和数据),加载到调用进程的地址空间中

让子进程执行其他程序

原理

将磁盘上的程序加载到内存,并和当前进程的页表,重新建立映射。

进程替换,没有创建新的子进程!!!

当子进程调用程序替换函数时,也是一种"写入",代码也要写时拷贝,让父子进程分离。

基本使用

int execl(const char* path,const char* arg,...);

path:程序所在路径+程序名

arg:可变参数列表 -> 可以传入不定个数的参数。命令行上程序怎么执行,这里的参数就怎么传

最后一个参数必须是NULL,表示传递完毕

调用失败,返回-1;调用成功,没有返回值

例如:让程序执行ls -a

arduino 复制代码
int main()    
{    
    printf("当前进程的开始\n");
    
    //程序替换完成后,会将当前进程所有代码和数据都进行替换,包括已经执行和没有执行的
    execl("/usr/bin/ls","ls","-a",NULL);                                                                                                                                                           //后续代码不会被执行
    printf("当前进程的结束\n");    
    return 0;    
}    

int execv(const char* path,char* const argv[]);

char* const argv[]:指针数组,里面保存一个个字符串;将命令行参数放进该数组

arduino 复制代码
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        char* const argv[] = {
            "ls",
            "-a",
            "-l",
            "-i",
            NULL  //注意还是以NULL结尾
        };
        //子进程
        execv("/usr/bin/ls",argv); //与execl只有传参方式的区别
        exit(1);
    }
    else{
        //父进程
        int status = 0;
        pid_t ret = waitpid(id,&status,0);//阻塞等待
        if(ret > 0){
            //等待成功
            printf("wait success,exit code:%d,%d\n",WEXITSTATUS(status),(status >> 8)&0xff);
        }
    }

    return 0;
}

int execlp(const char* file,const char* arg,...);

要执行一个程序,必须先通过路径找到。

没有指定路径,在环境变量PATH中查找

arduino 复制代码
//第一个"ls"代表:你要执行谁
//后面的参数代表:你想怎么执行
execlp("ls","ls","-l",NULL);

int execle(const char* path,const char* arg,...,char* const envp[]);

char* const envp[]:给替换的程序传递环境变量,传递后会覆盖之前子进程本身的环境变量

arduino 复制代码
//newproc.cc
#include <iostream>
using namespace std;
int main(int argc,char* argv[],char* env[])
{
    for(int i = 0; env[i] != nullptr;++i)
    {
        cout << env[i] << endl;
    }
    return 0;
}
scss 复制代码
    //myproc.cc
    pid_t id = fork();
    if (id == 0)
    {
        char* _env[]={
            "MYPROCCC=123",
            NULL
        };
        //子进程
        execle("./newproc","newproc",NULL,_env);
        exit(1);
    }

小结

exec*:是加载器的底层接口

l:理解为list,后面传命令行参数时,把参数一个一个往后传

v:理解为vector,把参数放到一个数组里传

p:理解为PATH,只需传递程序名,会自动在PATH环境变量的路径中找这个程序

e:理解为环境变量,程序替换后,传递新的环境变量,覆盖进程原来的环境变量

程序替换的系统调用:

以上的接口只是系统调用的封装

相关推荐
2401_854391081 分钟前
城镇住房保障:SpringBoot系统功能概览
java·spring boot·后端
陈随易5 分钟前
兔小巢收费引发的论坛调研Node和Deno有感
前端·后端·程序员
聪明的墨菲特i11 分钟前
Django前后端分离基本流程
后端·python·django·web3
Zfox_26 分钟前
【Linux】进程信号全攻略(二)
linux·运维·c语言·c++
安於宿命31 分钟前
【Linux】简易版shell
linux·运维·服务器
黄小耶@43 分钟前
linux常见命令
linux·运维·服务器
叫我龙翔44 分钟前
【计网】实现reactor反应堆模型 --- 框架搭建
linux·运维·网络
古驿幽情1 小时前
CentOS AppStream 8 手动更新 yum源
linux·运维·centos·yum
BillKu1 小时前
Linux(CentOS)安装 Nginx
linux·运维·nginx·centos
BillKu1 小时前
Linux(CentOS)yum update -y 事故
linux·运维·centos