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:理解为环境变量,程序替换后,传递新的环境变量,覆盖进程原来的环境变量

程序替换的系统调用:

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

相关推荐
ai小鬼头37 分钟前
AIStarter最新版怎么卸载AI项目?一键删除操作指南(附路径设置技巧)
前端·后端·github
Touper.42 分钟前
SpringBoot -- 自动配置原理
java·spring boot·后端
一只叫煤球的猫1 小时前
普通程序员,从开发到管理岗,为什么我越升职越痛苦?
前端·后端·全栈
一只鹿鹿鹿1 小时前
信息化项目验收,软件工程评审和检查表单
大数据·人工智能·后端·智慧城市·软件工程
M4K01 小时前
Linux百度网盘优化三板斧
linux
好奇的菜鸟2 小时前
如何在 Ubuntu 24.04 (Noble) 上使用阿里源
linux·运维·ubuntu
专注VB编程开发20年2 小时前
开机自动后台运行,在Windows服务中托管ASP.NET Core
windows·后端·asp.net
程序员岳焱2 小时前
Java 与 MySQL 性能优化:MySQL全文检索查询优化实践
后端·mysql·性能优化
bcbobo21cn2 小时前
初步了解Linux etc/profile文件
linux·运维·服务器·shell·profile
一只叫煤球的猫2 小时前
手撕@Transactional!别再问事务为什么失效了!Spring-tx源码全面解析!
后端·spring·面试