linux之进程控制

目录

一、fork函数

1.进程:

2.fork函数:

3.写时拷贝

4.fork常规用法

5.fork调用失败的原因

二、进程终止

1.终止是在做这么?

2.进程终止的3种情况

3.如何终止

三、进程等待

四、进程程序替换

1.替换原理

2.原理

3.将代码改成多线程

4.认识这些函数

1.execl

2.execv

3.execlp/execvp 

4.execle/execvpe


一、fork函数

1.进程:

        内核的相关管理数据结构(task_struct + mm_struct + 页表) + 代码和数据

2.fork函数:

        在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

进程调用fork,当控制转移到内核中的fork代码后,内核做:

        分配新的内存块和内核数据结构给子进程

        将父进程部分数据结构内容拷贝至子进程

        添加子进程到系统进程列表当中

        fork返回,开始调度器调度

fork函数返回值:子进程返回0,父进程返回的是子进程的pid。

为什么fork返回值是这样? 为了让父进程对子进程进行标识,知道子进程的pid,可以管理子进程。

3.写时拷贝

1.概念:

        通常,父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。

虚拟地址一样,物理地址却不一样。

4.fork常规用法

        一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。

        一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

5.fork调用失败的原因

        系统中有太多的进程

        实际用户的进程数超过了限制

二、进程终止

1.终止是在做这么?

        释放曾经的代码和数据所占据的空间

        释放内核数据结构

2.进程终止的3种情况

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

echo:内建命令,打印的都是bash内部的变量数据

? : 父进程bash获取到的,最近一个子进程退出的退出码

退出码 : 告诉关心方(父进程),我把任务完成的怎么样了

0:成功    !0 : 失败

1,2,3,4,5....不同的非0值,一方面表示失败,一方面表示失败的原因

for(int errnum = 0; errnum < 240; ++errnum)
       printf("%d -> %s\n",errnum, strerror(errnum));  //可以打印出每个数字代表的信息

为用户负责,可以了解错误信息

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


enum
{
  success=0,
  Div_zero,
  Mod_zero,
};    
    
const char* codeToerrstring(int code)    
{    
  switch(code)       
  {    
    case success:     
      return "success";    
      break;        
    case Div_zero:          
      return "Div_zero";    
      break;        
    case Mod_zero:     
      return "Mod_zero";    
      break;    
    default:    
      return "unknow error";    
      break;    
  }    
}    
    
int exit_code = 0;

int Div(int x, int y)
{
  if(x == 0)
  {
    exit_code = Div_zero;
    return -1;
  }
  else
  {
    exit_code = success;
    return x / y;
  }
}

int main()
{
  int result = Div(0,2);
  printf("result = %d , %s\n",result, codeToerrstring(exit_code));
  return exit_code;    
}
   

 代码跑完结果正不正确,可以通过进程的退出码来决定!

出错原因:系统 或者 自定义退出码。

代码执行时,出现异常,提前退出。

进程出异常,本质是进程收到OS发给进程的信号,提前终止

        一旦出现异常,退出码便没有意义。

1.先确定是否异常

2.不是异常,就一定是代码跑完了,看退出码就行

衡量一个进程退出,我们只需两个数字:退出码,退出信号!

退出码  退出信号 释义
0 0 运行成功
!0 0 运行结果不对
!0 !0 进程异常
0 !0

3.如何终止

1> main函数return,表示进程终止(非main函数 - return,函数结束)。

2> exit() -- C库函数。 代码调用exit函数。注:我们代码的任意位置调用exit,都表示进程终止。

3> _exit()  -- system call(系统调用) , exit会在进程退出时,冲刷缓冲区,而_exit不会。其它不变

注:exit底层调用_exit,目前我们所说的缓冲区,不是内核缓冲区。

三、进程等待

        结论:任何子进程,在退出的时候,一般必须要被父进程进行等待。进程在退出的时候,如果父进程不管不顾,退出进程,子进程就会进行状态Z(僵尸进程),导致内存泄漏

       why?

        1.父进程通过等待,解决子进程退出的僵尸问题,回收系统资源(一定要考虑的)

        2.获取子进程的退出信息,知道子进程是什么原因退出的。(可选的功能)

        

        怎么办?

        wait/waitpid

         pid_t wait(int *status),等待父进程时,任意一个子进程退出,即等待成功。 等待成功时,返回子进程的pid。

        如果子进程没有退出,父进程其实一直在进行阻塞等待!

        pid_t waitpid(pid_t pid, int *status, int option)

       返回值:当正常返回的时候waitpid返回手机的子进程的进程id

                      如果设置了选项WNOHANG,而调用中waitpid发现没有已退出

           的子进程可收集,则返回0 

                      如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。   

参数:pid: Pid = 1,等待人一个子进程。与wait等效

                    Pid = 0,等待其进程ID与pid相等的子进程。 

        int status (32位)

        退出信息:1.输出型参数 

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

void ChildRun()    
{    
  int cnt = 5;    
  while(cnt--)    
  {    
    printf("i am child, cnt:%d pid:%d, ppid:%d\n", cnt, getpid(), getppid());    
    sleep(1);    
  }    
}    
    
int main()    
{    
  printf("i am father, pid:%d, ppid:%d\n", getpid(), getppid());    
    
  pid_t id = fork();    
  if(id == -1) return 1;    
  if(id == 0)    
  {    
    //child    
    ChildRun();    
    printf("child quit...\n");    
    exit(1);    //异常退出
  }    
  sleep(7);    
  //parent    
  //pid_t rid = wait(NULL);    
  int status = 0;    
  pid_t rid = waitpid(id, &status, 0);    
  if(rid > 0)    
  {    
    printf("wait successful, rid:%d\n", rid);    
  }    
  else    
  {    
    printf("wait file!\n");    
  }    
  sleep(3);    
  printf("father quit,status:%d, child quit code:%d, child quit signal:%d\n",status, (status >> 8) & 0xFF, status & 0x7F);                           
  return 0;    
}

如果子进程是一个死循环,这时我们通过kill -9 3925发出一个信号终止子进程的话,就会得到

 如果我们在子进程中加入一个野指针异常,比如:

void ChildRun()      
{      
  int cnt = 5;      
  int* p =NULL;      
  while(1)      
  {      
    printf("i am child, cnt:%d pid:%d, ppid:%d\n", cnt, getpid(), getppid());            
    sleep(1);      
    *p =100;   //野指针                                                           
  }      
} 

 上述status使用了为运算的方式,对于运用不是很友好

        WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

        WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

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

void ChildRun()
{
  int cnt = 5;
  //int* p =NULL;
  while(cnt--)
  {
    printf("i am child, cnt:%d pid:%d, ppid:%d\n", cnt, getpid(), getppid());
    sleep(1);
    //*p =100;
  }
}

int main()
{
  printf("i am father, pid:%d, ppid:%d\n", getpid(), getppid());
  
  pid_t id = fork();
  if(id == -1) return 1;
  if(id == 0) 
  {
    //child
    ChildRun();
    printf("child quit...\n");
    exit(123);
  }
  //father
  while(1)
  {
    int status = 0;
    pid_t rid = waitpid(id, &status, WNOHANG);//non block
    
    if(rid == 0)
    {
      printf("child is running, father check next time!\n");
      usleep(1000);
      //DoOtherThing();
    }
    else if(rid > 0)
    {
      if(WIFEXITED(status))
      {
        printf("child quit success! child quit code:%d\n",WEXITSTATUS(status));
      } 
      else
      {
        printf("child quit unsuccess! child quit code:%d\n",WEXITSTATUS(status));
      }
      break;
    }
    else
    {
      printf("wait filed!\n");
    }
  }
  return 0;
}

WNOHANG:非阻塞式等待

四、进程程序替换

1.替换原理

        用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。


#include <unistd.h>

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

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

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

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

int execvp(const char *file, char *const argv[]);
 
int execvpe(const char *path, char *const argv[], char *const envp[]);

1.execl

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

int main()
{
    printf("begin execl...\n");
    execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
    printf("end execl...\n");
  return 0;
}

2.原理

进程的程序替换

        没有创建新的进程        

 站在被替换进程的角度,本质上就是这个程序被加载的内存中了

exec*系列的函数,执行完毕后,后续代码不见了,因为被替换了

execl函数的返回值不关心,因为替换成功,将不会向后继续运行,只要继续向后运行,则一定是运行失败了

类似“夺舍”。

3.将代码改成多线程

fork创建子进程,让子进程自己去替换 ,父进程wait

        既可以让子进程完成了任务,父进程还不受影响。

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

int main()
{
    printf("begin execl...\n");
    pid_t id = fork();
    if(id == 0)
    {
      //child
      sleep(2);
      execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
      exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0); //block
    if(rid > 0)
    {
      printf("father wait success! child exit code:%d\n", WEXITSTATUS(status));
    }
    printf("end execl...\n");
  return 0;
}

 父子进程具有独立性,当代码数据改变时,则子进程会发生写时拷贝。

4.认识这些函数

1.execl

 l:list 列表。    path:我们要执行的程序,需要带路径。

例:ls -a -l    execl(“/usr/bin/ls”,“ls”,“-a”,“-l”,NULL) ,结尾以NULL结束

2.execv

v:vector

int main()
{
    printf("begin execl...\n");
    pid_t id = fork();
    if(id == 0)
    {
      //child
      sleep(2);
      char *const argv[] = 
      {
        "ls",
        "-a",
        "-l",
        "--color",
        NULL,
      };
      execv("usr/bin/ls", argv);
      exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0); //block
    if(rid > 0)
    {
      printf("father wait success! child exit code:%d\n", WEXITSTATUS(status));
    }
    printf("end execl...\n");
  return 0;
}

3.execlp/execvp 

 p:用户可以不传要执行程序的路径(但是文件名要传),直接告诉它要执行谁就行。

        查找这个程序,系统会自动在系统环境变量PATH中查找

例:execlp("ls", "ls", "-a", "-l", NULL);          execvp("ls", argv);

4.execle/execvpe

e:environment


回到execl,我创建一个程序,并且要在testexec.c中运行它,并且打印它的pid

它们的pid均为22166,证明没有创建新的进程。


envp:整体替换所有的环境变量

1.用全新的自己写的给子进程

2.用老的环境变量给子进程,enverion

3.老的环境变量稍微修改,给子进程  putenv("");

5.真正的系统调用:execve

其它exec函数都是execve的封装,底层都是调用的这个函数

上一篇:智能合约测试例子


下一篇:8分钟搞懂Java中的各种锁