Linux进程创建,进程终止,进程等待,进程程序替换,简易shell代码

进程创建

fork函数初识

fork函数:从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程

#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1
进程调用 fork ,当控制转移到内核中的 fork 代码后,内核做:
1 分配新的内存块和内核数据结构给子进程(简单来说,即创建子进程的PCB,地址空间,页表,构建映射关系)
2 将父进程部分数据结构内容拷贝至子进程
3 添加子进程到系统进程列表当中
4 fork返回,开始调度器调度
下面看一段代码:
  4 int main()
  5 {
  6   printf("Before: pid is %d\n", getpid());
  7   pid_t id = fork();
  8 
  9   if(id==-1)
 10   {
 11     perror("fork()");
 12     exit(1);
 13   }
 14 
 15   printf("After:pid is %d, fork return %d\n", getpid(), id);
 16   sleep(1);
 17   return 0;
 18 }

fork 之前父进程独立执行, fork 之后,父子两个执行流分别执行。注意, fork 之后,谁先执行完全由调度器决定

写时拷贝

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

当父进程形成子进程之后,子进程写入,就要发生写时拷贝(重新申请空间,进行拷贝,修改页表(由OS完成)),问题:子进程正在写入,怎样完成写时拷贝的操作?

原因:父进程创建子进程的时候,首先将自己的页表中的读写权限改成只读,然后再创建子进程

以上这一点,用户不知道,用户可能对某一批数据进行写入,那么对只读权限的数据进行写入操作,此时页表的虚拟地址向物理地址转换过程,会因为权限问题而出错,操作系统OS就可以介入了,出错分两类:

1 真的出错   如对代码区进行写入操作

2 不是真的出错   会触发我们进行重新申请内存,拷贝内容的策略机制(写时拷贝)

fork常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求
一个进程要执行一个不同的程序。例如子进程从 fork 返回后,调用 exec 函数

fork调用失败的原因

系统中有太多的进程

实际用户的进程数超过了限制
创建多进程:
  5 #define N 10
  6 
  7 void Worker()
  8 {
  9     int cnt = 10;
 10     while(cnt)
 11     {
 12         printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
 13         sleep(1);                                            
 14         cnt--;                                               
 15     }                                                        
 16 }                                                            
 17                                                              
 18 int main()                                                   
 19 {                                                            
 20   for(int i = 0;i<N;i++)                                     
 21   {                                                          
 22     sleep(1);                                                
 23     pid_t id = fork();                                       
 24     if(id==0)                                                
 25     {                                                        
 26        printf("create child process success: %d\n", i);      
 27        // child                                                                                                                       
 28        Worker();                                             
 29        exit(0);                                              
 30     }
 31   }
 32                                                              
 33   //只有父进程走到这里
 34   sleep(100);
 35   
 36   return 0;
 37 }
                     

监控:

 改进以上代码:

  8 #define N 10
  9 
 10 typedef void (*callback_t)();
 11 
 12 void Worker()
 13 {
 14     int cnt = 10;
 15     while(cnt)
 16     {
 17         printf("I am child process, pid: %d, ppid: %d, cnt: %d\    n", getpid(), getppid(), cnt);
 18         sleep(1);
 19         cnt--;
 20     }
 21 }
 22 
 23 void createSubPorcess(int n, callback_t cb)
 24 {
 25     for(int i = 0; i < n; i++)
 26     {                                                          
 27         sleep(1);
 28         pid_t id = fork();          
 29         if(id == 0)
 30         {                           
 31             printf("create child process success: %d\n", i);
 32             // child
 33             cb();
 34             exit(0);
 35         }
 36     }
 37 }
 38 
 39 
 40 int main()
 41 {
 42   createSubPorcess(N, Worker);
 43   //只有父进程走到这里
 44   sleep(100);
 45   return 0;
 46 }

进程终止

main函数也是一个函数,由返回值,我们平常main函数返回的都是0,可以返回其他值吗?当然是可以的,我们可以用echo $?来查看main函数的返回值

?:保存的是最近一个子进程执行完毕时的退出码

 main函数的返回值,叫做进程的退出码,0表示成功,非0表示失败

进程退出场景

1 代码运行完毕,结果正确

2 代码运行完毕,结果不正确

代码异常终止

在多进程环境中,父进程创建子进程的目的是,帮它办事

进程常见退出方法

正常终止(可以通过 echo $? 查看进程退出码):

1. main返回

2. 调用exit

3. _exit

异常退出:ctrl + c,信号终止

退出码

父进程可以通过获得子进程的退出码即main函数的返回值,用来判断子进程的运行结果(来得知子进程把事情办得怎么样)

当某进程的退出码为非0,我们需要知道这个进程是因为什么原因失败的(如退出码为1,2,3,4)我们就可用不同的数字表示不同的原因

纯数字能够表示错误原因,但是不直观,所以我们需要将退出码的纯数字形式转换成退出码的字符串形式,strerror就具有这种功能(内置的转换),当然我们也可以自定义将纯数字的退出码定义成我们想要表达的字符串形式

 42   for(int i = 0;i<200;i++)
 43   {
 44     printf("%d: %s\n",i,strerror(i));
 45   }

 40 int main()
 41 {
 42   for(int i = 0;i<200;i++)
 43   {
 44     printf("%d: %s\n",i,strerror(i));
 45   }
 46   return 10;
 47 } 

 

现在最近执行完成的子进程是echo $?命令,它是成功执行的,所以退出码是0 

错误码errno

当某个库函数/系统调用接口出错的时候会被自动设置 

 42   printf("before: %d\n", errno);      
 43   FILE *fp = fopen("./log.txt", "r");//没有这个文件
 44   printf("after: %d, error string : %s\n", errno, strerror(errno));  

退出码vs错误码 

错误码:通常是衡量一个库函数或者是一个系统调用接口的调用情况

当失败的时候,用来衡量函数的详细出错原因

退出码:通常是一个进程退出的时候,它的退出结果

当失败的时候,用来衡量进程出错的详细出错原因

我们可以将退出码与错误码设置成一样的:

   40 int main()                                
   41 {                                         
   42   int ret = 0;                            
   43   printf("before: %d\n", errno);
   44   FILE *fp = fopen("./log.txt", "r");//没有这个文件         
   45   if(fp == NULL)                                            
   46   {                 
   47     printf("after: %d, error string : %s\n", errno, strerror(      errno));
   48     ret = errno;
   49   }
   50   return ret;                                                
   51 } 

进程异常 

进程的退出场景中还有一条是:代码异常终止

程序运行起来就是一个进程了,程序崩溃,其实就是进程异常,一旦进程异常,就不会再继续运行了,即被操作系统杀掉(也只能被OS杀掉),(因为操作系统是进程的管理者)

操作系统通过信号的方式来杀掉进程

除零错误:

int a = 10;
 a/=0;  

其实也就是8号信号:SIGFPE 

信号: 

 当进程出现异常,异常信息会被操作系统检测到,进而被操作系统转换成信号把这个进程杀掉

我们可以给正常的进程发送某些信号:

    while(1)
    {
        printf("I am a normal process: %d\n", getpid());
        sleep(1);
    }

 进程出异常,本质是进程收到了对应的信号,自己终止了

所以一个进程是否出现异常,我们只要看有没有收到信号即可

信号与退出码都是数字,信号没有0号,所以判断是否为信号,只需要看它是否为0

exit函数

参数是进程的退出码,类似于main函数的return

 42   printf("I am a process, pid: %d, ppid: %d\n",getpid(), getppid());                           
 43   exit(10); 

 

任意地点调用exit,表示进程退出,不进行后续执行

 39 void func()
 40 {
 41   printf("call func()\n");
 42   exit(6);
 43 }
 44 
 45 int main()
 46 {
 47   func();
 48   printf("I am a process, pid: %d, ppid: %d\n",getpid(), getppid());
 49   exit(10);                                                                                                                           
 50 }          

 

exit最后也会调用_exit 

执行 return n 等同于执行 exit(n), 因为调用 main 的运行时函数会将 main 的返回值当做 exit 的参数

_exit函数

#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值

 39 void func()
 40 {
 41   printf("call func()\n");
 42   _exit(6);
 43 }
 44 
 45 int main()
 46 {
 47   func();
 48   printf("I am a process, pid: %d, ppid: %d\n",getpid(), getppid());
 49   _exit(10);                                                                                                                          
 50 }

exit与_exit都能用来终止进程

 47   printf("hello\n");                                                                           
 48   sleep(3);                                                                                    
 49   exit(1);   

 

现象:hello字符串一开始就显示 

去掉上面代码中的\n

 47   printf("hello");                                                                                                                    
 48   sleep(3);
 49   exit(1);

 

现象:一开始hello字符串并没有显示出来,当进程退出后才显示 

printf向显示器打印是行刷新,因为没有\n所以不会立即显示

结论:exit在终止进程时会刷新缓冲区

 47   printf("hello");
 48   sleep(3);
 49   _exit(1);      

现象:进程结束后没有看到字符串hello

exit vs _exit

 1 exit是库函数 _exit是系统调用

 2 exit终止进程的时候,会自动刷新缓冲区  _exit终止进程的时候,不会自动刷新缓冲区

我们目前知道的缓冲区,绝对不再操作系统内部(否则exit()能刷新,_exit()也应该要刷新),这个缓冲区在C标准库里

所以当我们调用exit()时,会先把对应的数据写到操作系统里,然后再刷新

但是调用_exit()时,数据是在C缓冲区的,数据不在操作系统里,所以终止进程时不会刷新

进程等待

什么是进程等待

通过wait/waitpid的方式,让父进程(一般是父进程)对子进程进行资源回收的等待过程

为什么要进行等待

1 解决子进程僵尸问题带来的内存泄露问题(必须)

2 父进程创建子进程的目的是让子进程完成任务,子进程将任务完成得如何

   父进程需要知道--->通过进程等待的方式获取子进程的退出信息--两个数字(信号编码,退出码)

   (退出码看运行结果的正确与否,信号编码看进程异常与否)

   (不是必须,但是系统需要提供这样的基础功能)

如何进行等待

wait:

一次等待任意一个子进程的退出

返回值:
成功返回被等待进程 pid ,失败返回 -1
参数:
输出型参数,获取子进程退出状态 , 不关心则可以设置成为 NULL

   14 void Worker()
   15 {
   16     int cnt = 5;
   17     while(cnt)
   18     {
   19         printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
   20         sleep(1);
   21     }
   22 }

   23 
   24 
   25 int main()
   26 {
   27   pid_t id = fork();
   28   if(id==0)
   29   {
   30     //child                                                                                                                         
   31     Worker();
   32     exit(0);
   33   }
   34   else 
   35   {
   36     sleep(10);
   37     //father
   38     pid_t rid = wait(NULL);
   39     if(rid==id)
   40     {
   41       printf("wait success, pid: %d\n",getpid());
   42     }
   43 
   44     sleep(10);
   45   }
   46   return 0;
   47 }

 所以进程等待能够回收子进程的僵尸状态

下一个问题:子进程运行期间,父进程有没有调用wait呢?父进程在干什么?

​
   14 void Worker()
   15 {
   16     int cnt = 5;
   17     while(cnt)
   18     {
   19         printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
   20         sleep(1);
   21     }
   22 }

   23 
   24 
   25 int main()
   26 {
   27   pid_t id = fork();
   28   if(id==0)
   29   {
   30     //child                                                                                                                         
   31     Worker();
   32     exit(0);
   33   }
   34   else 
   35   {
   37     //father
          printf("wait before\n");
   38     pid_t rid = wait(NULL);
          printf("wait after\n");
   39     if(rid==id)
   40     {
   41       printf("wait success, pid: %d\n",getpid());
   42     }
   43 
   44     sleep(10);
   45   }
   46   return 0;
   47 }

​

 

如果子进程根本没有退出,父进程必须在wait上进行阻塞等待,直到子进程僵尸,wait自动回收,返回

一般而言,父子进程谁先运行不知道,由调度器决定,但是一般都是父进程最后退出

waitpid:

waitpid有wait一样的功能,此外还可获取退出信息

返回值:
当正常返回的时候 waitpid 返回收集到的子进程的进程 ID
如果设置了选项 WNOHANG, 而调用中 waitpid 发现没有已退出的子进程可收集 , 则返回 0
如果调用中出错 , 则返回 -1, 这时 errno 会被设置成相应的值以指示错误所在;

参数:
pid
Pid=-1, 等待任一个子进程。与 wait 等效。
Pid>0. 等待其进程 ID pid 相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进程的ID

获取子进程status

waitwaitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充

如果传递NULL,表示不关心子进程的退出状态信息

否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程

其中status是一个输出型参数

 14 void Worker()
 15 {
 16     int cnt = 5;
 17     while(cnt)
 18     {
 19         printf("I am child process, pid: %d, ppid: %d, cnt: %d\    n", getpid(), getppid(), cnt--);
 20         sleep(1);
 21     }
 22 }

 25 int main()
 26 {
 27   pid_t id = fork();
 28   if(id==0)
 29   {
 30     //child
 31     Worker();
 32     exit(10);
 33   }
 34   else
 35   {
 36      printf("wait before\n");
 37     //father
 38     int status = 0;
 39     pid_t rid = waitpid(id,&status,0);
 40     printf("wait after\n");
 41     if(rid==id)
 42     {
 43       printf("wait success, pid: %d, status: %d\n",getpid(),status);                                                                  
 44     }                                                                                                           
 45                                                                                                                 
 46     sleep(10);                                                                                                  
 47   }                                                                                                             
 48   return 0;                                                                                                     
 49 }         

 

子进程的退出码是10,status为什么为2560?

status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status 16 比特位):

所以要想获得退出信息(信号编码与退出码) 就不能对status整体使用

 

进程异常的场景:子进程执行的代码除0错误

 

 当一个进程异常(收到了信号)退出码exit code就无意义了

判断一个进程是否异常,只需要看信号编码exit sig是否为0(0表示正常,非0表示异常,因为信号编码都是非0的)

父进程是如何通过wait/waitpid获取子进程的退出信息(退出码与退出信号)呢?

当然啦,以上方式获取status中的退出信号与退出码优点麻烦,这里有更轻松的方式:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
   14 void Worker()
   15 {
   16     int cnt = 5;
   17     while(cnt)
   18     {
   19         printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
   20         sleep(1);
   21         int a = 10;//除0错误,进程异常
   22         a/=0;
   23     }
   24 }


   27 int main()
   28 {
   29   pid_t id = fork();
   30   if(id==0)
   31   {
   32     //child       
   33     Worker();     
   34     exit(0);                                                                                                                       
   35   }
   36   else 
   37   {
   38      printf("wait before\n");
   39     //father
   40     int status = 0;
   41     pid_t rid = waitpid(id,&status,0);
   42     printf("wait after\n");
   43     if(rid==id)
   44     {
   45       if(WIFEXITED(status))
   46       {
   47          printf("child process normal quit, exit code : %d\n", WEXITSTATUS(status));
   48       }
   49       else 
   50       {
   51          printf("child process quit except!\n");
   52       }
   53     }
   54 
   55     sleep(10);
   56   }
   57   return 0;

将以上代码稍加改动,去掉除0的代码,恢复成正常代码,让进程正常终止

 

等待多个进程:

  6 const int n = 10;
  7 
  8 void Worker(int number)
  9 {
 10   int cnt = 10;
 11   while(cnt)
 12   {
 13     printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %d\n", getpid(), getppid(), cnt--, number);
 14     sleep(1); 
 15   }
 16 }

 18 int main()
 19 {
 20     for(int i = 0;i<n;i++)
 21     {
 22       pid_t id = fork();
 23       if(id==0)
 24       {
 25         Worker(i);
 26         exit(i);
 27       }                                                                                                                               
 28     }
 29 
 30     //等待多个子进程
 31     for(int i = 0;i<n;i++)
 32     {
 33       int status = 0;
 34       pid_t rid = waitpid(-1,&status,0);
 35       if(rid>0)
 36       {
 37         printf("wait child %d success, exit code: %d\n", rid, WEXITSTATUS(status));
 38       }
 39     }
 40   return 0;
 41 }

 

父进程调用wait/waitpid 为阻塞式调用时:子进程不退出,wait/waitpid不返回(阻塞等待)

                                                                   父进程什么都干不了

为非阻塞式调用时: 若是等待的条件不满足,wait/waitpid不阻塞,立即返回(非阻塞等待)

                                  往往要重复调用

我们通常采用:轮询+非阻塞式方案,进行进程等待(即将wait放在while循环中,一直去检测)

                         优点在于:在进行等待的过程中,可以顺便做一下占据时间并不多的某些事情

waitpid的返回值:

返回值>0: 等待成功

返回值==0:等待是成功的,但是对方还没有退出

返回值<0:等待失败 

一个简单的非阻塞式等待代码:

    #define TASK_NUM 
    typedef  void (*task_t)();
  8 void worker(int cnt)
  9 {
 10   printf("I am child,pid: %d,ppid: %d\n,cnt: %d\n",getpid(),getppid(),cnt);
 11 }

    void download()
    {
       printf("this is a download task is running!\n");
    }
    
    void printLog()
    {
       printf("this is a write log task is running!\n");
    }
    
    void show()
    {
       printf("this is a show info task is running!\n");
    }
    
    void executeTask(task_t tasks[],int num)
    {
        for(int i = 0;i<num;i++)
        { 
           if(tasks[i])
           {
              tasks[i]();
           }
        }
    }


    int addTask(task_t tasks[],task_t t)
    {
       int i = 0;
       for(; i<TASK_NUM;i++)
       {
          if(tasks[i]==NULL)
          {
             tasks[i] = t;
             return 1;
          }
       }
       return 0;
    }

    void InitTasks(task_t tasks[],int num)
    {
        for(int i = 0;i<num;i++)
        {
            tasks[i] = NULL;
        }
    }
       
 12 
 13 int main()
 14 {
      task_t tasks[TASK_NUM];
      InitTasks(tasks,TASK_NUM);
      addTask(tasks,download);
      addTask(tasks,printLog);
      addTask(tasks,show);
 15   pid_t id = fork();
 16   if(id==0)
 17   {
 18     //child
 19     int cnt = 5;
 20     while(cnt)
 21     {
 22       worker(cnt);
 23       sleep(2);
 24       cnt--;
 25     }                                                          
 26                                                                
 27     exit(0);                                                   
 28   }   
  
 30   while(1)
 31   {
 32     //father
 33     int status = 0;
 34     pid_t rid = waitpid(id,&status,WNOHANG);
 35     if(rid>0)
 36     {
 37       //wait success,child quit now
 38       printf("child quit success, exit code: %d, exit signal: %d\n",(status>>8)&0xFF,status&0x7F);
 39       break;
 40     }
 41     else if(rid==0)
 42     {
 43       //wait success,but child not quit
          printf("########################\n");
 44       printf("child is alive,wait again,father do other thing...\n");
          executeTask(tasks,TASK_NUM);
          printf("########################\n");                                                                 
 45     }
 46     else 
 47     {
 48       //wait failed,child unknow
 49       printf("wait failed!\n");
 50       break;
 51     }
 52 
 53     sleep(1);
 54   }
 55   return 0;
 56 }
  

非阻塞等待,可以让等待方在wait返回时,顺便做做自己的事情

进程程序替换

我们所创建的所有子进程,它们执行的代码,都是父进程代码的一部分,只不过采用了if else进行分流,如果我们想要子进程执行新的程序呢?执行全新的代码,访问全新的数据,不再与父进程有瓜葛?答案:程序替换

下图为父子进程有瓜葛:

 单进程版的程序替换代码(无子进程)

--见见程序替换

  1 #include<unistd.h>
  2 #include<stdio.h>
  3 
  4 int main()
  5 {
  6   printf("pid: %d,exec command begin\n",getpid());
  7   execl("/usr/bin/ls","ls","-a","-l",NULL);
  8   printf("pid: %d,exec command end\n",getpid());                                                                                      
  9   return 0;                                                                                                                   
 10 } 

 

现象:我们可以用“语言”调用其他程序

怎么不显示以下这一条语句??后面回答

程序替换的原理 

每个进程运行起来就要有自己的task_struct,mm_struct(地址空间) 页表

程序替换就是:用新程序的代码和数据替换调用exec系列函数的程序的代码与数据

其中并没有创建新进程 

多进程版的程序替换代码:

    5 int main()
    6 {
    7   pid_t id = fork();
    8   if(id==0)
    9   {
   10     //child 
   11     printf("pid: %d,exec command begin\n",getpid());
   12     sleep(3);                                                                                                                       
   13     execl("/usr/bin/ls","ls","-a","-l",NULL);                                              
   14     printf("pid: %d,exec command end\n",getpid());                                         
   15   }                                                                                        
   16   else                                                                                     
   17   {                                                                                        
   18     //father                                                                               
   19     pid_t rid = waitpid(-1,NULL,0);                                                         
   20     if(rid>0)                                                                              
   21     {                                                                                      
   22       printf("wait success,rid: %d\n",rid);                                                
   23     }                                                                                      
   24   }                                                                                        
   25   return 0;                                                                                
   26 } 

 

进程具有独立性,父进程fork创建出子进程,代码和数据共享,当新程序的代码与数据要替换子进程的代码和数据时,会发生写时拷贝,让父子进程的代码和数据各自私有一份,那么子进程的代码和数据被替换则不会影响父进程

子进程怎么知道要从新程序的最开始执行?它怎么知道最开始的地方在哪里?

exce系列的函数会读取新程序(磁盘中的文件,EIF格式)头部字段entry的内容:可执行程序的入口地址,将其写到调用exec系列函数的程序的eip(CPU内的寄存器,记录该程序执行到哪里了)中,这样原程序就执行了新程序的内容 

 exec系列的函数,如果当前进程调用执行成功,那么后续代码没有机会执行了,因为被替换掉了

exec系列的函数只有失败的返回值,无成功的返回值

 所以对于execl系列的函数,不用判断,只要执行exec系列函数后续的代码就是出错

正确写法:

程序替换的方法

 程序替换的要解决的本源问题:

1 必须先找到这个可执行程序

2 必须告诉exec系列的函数怎么执行

其实有六种以 exec 开头的函数 , 统称 exec 函数 :
#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 execve(const char *path, char *const argv[], char *const envp[]);
这些函数如果调用成功则加载新的程序从启动代码开始执行 , 不再返回。
如果调用出错则返回 -1
所以 exec 函数只有出错的返回值而没有成功的返回值。
命名理解

execl:

execlp: 

  6 int main()
  7 {
  8   pid_t id = fork();
  9   if(id==0)
 10   {
 11     //child 
 12     printf("pid: %d,exec command begin\n",getpid());
 13     sleep(1);
 14     execlp("ls","ls","-a","-l",NULL);                                                                                                 
 15     printf("pid: %d, exec command end\n",getpid());            
 16     exit(1);                                   
 17   }                                            
 18   else                                         
 19   {                                            
 20     //father                               
 21     pid_t rid = waitpid(-1,NULL,0);        
 22     if(rid>0)                              
 23     {                                      
 24       printf("wait success,rid: %d\n",rid);
 25     }                                      
 26   }                                        
 27   return 0;                                
 28 }                        

 

execv:

 

    6 int main()
    7 {
    8   pid_t id = fork();
    9   if(id==0)
   10   {
   11     char* const argv[] = {
   12       "ls",
   13       "-a",
   14       "-l",
   15       NULL
   16     };
   17     //child 
   18     printf("pid: %d,exec command begin\n",getpid());                                                                                
   19     sleep(3);
   20     execv("/usr/bin/ls",argv);
   21     printf("pid: %d, exec command end\n",getpid());
   22     exit(1);
   23   }
   24   else
   25   {
   26     //father
   27     pid_t rid = waitpid(-1,NULL,0);
   28     if(rid>0)
   29     {
   30       printf("wait success,rid: %d\n",rid);
   31     }
   32   }
   33   return 0;
   34 }

 

execvp:

 

    6 int main()
    7 {
    8   pid_t id = fork();
    9   if(id==0)
   10   {
   11     char* const argv[] = {
   12       "ls",
   13       "-a",
   14       "-l",
   15       NULL
   16     };
   17     //child 
   18     printf("pid: %d,exec command begin\n",getpid());
   19     sleep(3);
   20     execvp(argv[0],argv);                                                                                                           
   21     printf("pid: %d, exec command end\n",getpid());                   
   22     exit(1);                                                          
   23   }                                                                   
   24   else                                                                
   25   {                                                                   
   26     //father                                                          
   27     pid_t rid = waitpid(-1,NULL,0);                                   
   28     if(rid>0)                                                         
   29     {                                                                 
   30       printf("wait success,rid: %d\n",rid);                           
   31     }                                                                 
   32   }                                                                   
   33   return 0;
   34 }

 

程序替换能替换系统的指令程序,当然也能替换我们自己写的程序 

mytest2.cc

 Makefile:一次形成两个可执行程序的做法

现在用我们自己写的c程序将c++程序调用起来:

mytest.c:

  6 int main()  
  7 {  
  8   pid_t id = fork();  
  9   if(id==0)  
 10   {  
 11     //child   
 12     printf("pid: %d,exec command begin\n",getpid());  
 13     sleep(3);  
 14     execl("./mytest2","mytest2",NULL);                                                                                                
 15     printf("pid: %d, exec command end\n",getpid());                                               
 16     exit(1);                                                                                      
 17   }                                                                                               
 18   else                                                                                 
 19   {                                                                                    
 20     //father                                                                           
 21     pid_t rid = waitpid(-1,NULL,0);                                                    
 22     if(rid>0)                                                                          
 23     {                                                                                  
 24       printf("wait success,rid: %d\n",rid);                                            
 25     }                                                                                  
 26   }                                                                                    
 27   return 0;                                                                         
 28 }                   

 

exec系列的函数能够执行程序的进程替换,所以除了上面用c程序调用c++程序外,我们还可以用c程序调用脚本程序 

1 当我们进行程序替换的时候,子进程对应的环境变量,是可以直接从父进程来的

 儿子进程的环境变量从父进程继承下来,孙子进程的环境变量从儿子进程继承下来

验证:

 

 在儿子进程中导入环境变量:使用putenv

 

儿子进程新增的环境变量不会影响父进程:

 bash中没有MYVAL2

2 环境变量被子进程继承下去是一种默认行为,不受程序替换的影响

  因为通过地址空间可以让子进程继承父进程的环境变量数据

  疑问:环境变量与命令行参数也是数据,exec函数会替换进程的代码与数据,为什么环境变量不受程序替换的影响?

答:程序替换,环境变量不会被替换

让子进程执行的时候,获得的环境变量:

1 将父进程的环境变量原封不动传给子进程

   a 直接用 b 直接传

execle:

其中传递的envp不是新增式传递,而是覆盖式传递

 mytest.c

    extern char**environ;
  6 int main()  
  7 {  
  8   pid_t id = fork();
  9   if(id==0)
 10   {
 11     //child 
 12     printf("pid: %d,exec command begin\n",getpid());
 13     execle("./mytest2","mytest2",NULL,environ);                                                                                       
 14     printf("pid: %d, exec command end\n",getpid());                                                                         
 15     exit(1);                                                                                                                
 16   }                                                                                                                         
 17   else                                                                                                                      
 18   {                                                                                                                         
 19     //father                                                                                                                
 20     pid_t rid = waitpid(-1,NULL,0);                                                                                         
 21     if(rid>0)                                                                                                               
 22     {                                                                                                                       
 23       printf("wait success,rid: %d\n",rid);                                                                                 
 24     }                                                                                                                       
 25   }                                                                                                                         
 26   return 0;                                                                                                                 
 27 }                                  

 

2 想要传递自己的环境变量 

我们可以直接构造环境变量表,给子进程传递

mytest.c

 9 int main()
 10 {
 11   char *const myenv[] ={
 12       "MYVAL1=11111111111111",
 13       "MYVAL2=11111111111111",
 14       "MYVAL3=11111111111111",
 15       "MYVAL4=11111111111111",
 16       NULL
 17   };
 18 
 19   pid_t id = fork();
 20   if(id==0)
 21   {
 22     //child 
 23     printf("pid: %d,exec command begin\n",getpid());
 24     execle("./mytest2","mytest2",NULL, myenv);                                                                                        
 25     printf("pid: %d, exec command end\n",getpid());                     
 26     exit(1);                                                            
 27   }                                        
 28   else
 29   {         
 30     //father                       
 31     pid_t rid = waitpid(-1,NULL,0);
 32     if(rid>0)
 33     {
 34       printf("wait success,rid: %d\n",rid);
 35     }
 36   }
 37   return 0;
 38 }

 3 想要新增式传递环境变量

使用putenv在当前进程导入环境变量,可以传给它的子进程

 总结:

程序替换可以将命令行参数和环境变量通过自己的参数传递给被替换的程序的main函数中

尤其是环境变量,采用覆盖式传递

execve:

设计简易shell

shell / bash 从用户读入字符串 "ls" shell / bash 建立一个新的进程(子进程),然后在子进程中运行 ls 程序并等待子进程结束。

要写一个shell,需要循环以下过程:

1. 获取命令行
2. 解析/分割命令行
3. 建立一个子进程( fork
4. 替换子进程( execvp

 5. 父进程等待子进程退出(wait/waitpid

Makefile:

  1 mybash:myshell.c                                                                                                                      
  2   gcc -o $@ $^ -std=c99
  3 .PHONY:clean
  4 clean:
  5   rm -f mybash

myshell.c

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3 #include <string.h>
    4 #include <unistd.h>
    5 #include <sys/types.h>
    6 #include <sys/wait.h>
    7 
    8 
    9 #define NUM 1024
   10 #define SIZE 64
   11 #define SEP " "
   12 //#define Debug 1
   13 
   14 int lastcode = 0;//最近一个进程的退出码
   15 char enval[1024];//设置环境变量
   16 char cwd[1024];//设置环境变量PWD的值
   17 
   18 const char *getUsername()
   19 {
   20     const char *name = getenv("USER");
   21     if(name) return name;
   22     else return "none";
   23 }
   24 const char *getHostname()
   25 {
   26     const char *hostname = getenv("HOSTNAME");
   27     if(hostname) return hostname;                                                                                                   
   28     else return "none";
   29 }
   30 const char *getCwd()
   31 {
   32     const char *cwd = getenv("PWD");
   33     if(cwd) return cwd;
   34     else return "none";
   35 }
   36 
   37 int getUserCommand(char *command, int num)
   38 {
   39    printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());                                                                  
   40    char *r = fgets(command, num, stdin); // 最终你还是会输入\n
   41    if(r == NULL) return -1;
   42     // "abcd\n" 处理'\n'
   43    command[strlen(command) - 1] = '\0'; //不会越界,至少会有\n
   44    return strlen(command);
   45 } 
   46 
   47 void commandSplit(char *in, char *out[])
   48 {
   49     int argc = 0;
   50     out[argc++] = strtok(in, SEP);
   51     while( out[argc++] = strtok(NULL, SEP));
   52 
   53 #ifdef Debug
   54     for(int i = 0; out[i]; i++)
   55     {
   56         printf("%d:%s\n", i, out[i]);
   57     }
   58 #endif
   59 }
   60 
   61 int execute(char *argv[])
   62 {
   63     pid_t id = fork();                                                                                                              
   64     if(id < 0) return -1;
   65     else if(id == 0) //child
   66     {
   67         // exec command
   68         execvp(argv[0], argv); // cd ..
   69         exit(1);
   70     }
   71     else // father
   72     {
   73         int status = 0;
   74         pid_t rid = waitpid(id, &status, 0);
   75         if(rid > 0){
   76             lastcode = WEXITSTATUS(status);
   77         }
   78     }
   79 
   80     return 0;
   81 }
   82 
   83 
   84 char *homepath()
   85 {
   86     char *home = getenv("HOME");
   87     if(home) return home;
   88     else return (char*)".";
   89 }
   90 
   91 
   92 void cd(const char *path)
   93 {                                                                                                                                   
   94     chdir(path);
   95     char tmp[1024];
   96     getcwd(tmp, sizeof(tmp));
   97     sprintf(cwd, "PWD=%s", tmp); 
   98     putenv(cwd);
   99 }
  100 
  101 //bash自己执行内建命令,类似于bash内部的一个函数
  102 int doBuildin(char *argv[])
  103 {
  104     if(strcmp(argv[0], "cd") == 0)
  105     {
  106         char *path = NULL;
  107         if(argv[1] == NULL) path=homepath();
  108         else path = argv[1];
  109         cd(path);
  110         return 1;
  111     }
  112     else if(strcmp(argv[0], "export") == 0)
  113     {
  114         if(argv[1] == NULL) return 1;
  115         strcpy(enval, argv[1]);
  116         putenv(enval); 
  117         return 1;
  118     }
  119     else if(strcmp(argv[0], "echo") == 0)
  120     {                                                                                                                               
  121         if(argv[1] == NULL)//为空
  122         {
  123             printf("\n");
  124             return 1;
  125         }
  126         if(*(argv[1]) == '$' && strlen(argv[1]) > 1)//不为空,且紧跟$
  127         { 
  128             char *val = argv[1]+1; //1. $PATH(环境变量) $?(退出码)
  129             if(strcmp(val, "?") == 0)
  130             {
  131                 printf("%d\n", lastcode);
  132                 lastcode = 0;
  133             }
  134             else
  135             {
  136                 const char *enval = getenv(val);
  137                 if(enval) printf("%s\n", enval);
  138                 else printf("\n");
  139             }
  140             return 1;
  141         }
  142         else 
  143         {
  144             printf("%s\n", argv[1]);
  145             return 1;
  146         }
  147     }
  148     //.......其他内建命令
  149 
  150     return 0;                                                                                                                       
  151 }
  152 
  153 
  154 
  155 int main()
  156 {
  157   while(1)
  158   {
  159        // 1. 打印提示符&&获取用户命令字符串获取成功
  160        char usercommand[NUM];
  161        int n = getUserCommand(usercommand, sizeof(usercommand));
  162        if(n <= 0) continue;
  163        // 2. 分割字符串
  164        // "ls -a -l" -> "ls" "-a" "-l"
  165        char *argv[SIZE];
  166        commandSplit(usercommand, argv);
  167        // 3. check build-in command 检查并让mybash执行内建命令
  168         n = doBuildin(argv);
  169         if(n) continue;
  170          // 4. 子进程执行对应的命令
  171         execute(argv);
  172   }
  173 }


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值