• 【Linux】六、进程控制


    进程创建

    fork函数的认识

    #include

    pid_t fork(void)

    1. #include
    2. #include
    3. int main()
    4. {
    5. printf("我是父进程!\n");
    6. pid_t id = fork();
    7. if(id < 0)
    8. {
    9. printf("创建子进程失败\n");
    10. return 1;
    11. }
    12. else if
    13. {
    14. //子进程
    15. while(1)
    16. {
    17. printf("我是子进程:pid: %d, ppid: %d\n",getpid(),getppid());
    18. sleep(1);
    19. }
    20. }
    21. else
    22. {
    23. //父进程
    24. while(1)
    25. {
    26. printf("我是子进程:pid: %d, ppid: %d\n",getpid(),getppid());
    27. sleep(1);
    28. }
    29. }
    30. return 0;
    31. }

    请描述一下,fork创建子进程,操作系统都做了什么?

    fork创建子进程就是系统里多了一个进程,也就是进程=内核数据结构+进程代码和数据;一般从磁盘中来,也就是C\C++程序加载后的结果;分配新的内存块和内核数据结构给子进程;将父进程部分数据结构内容拷贝到到子进程;添加子进程到系统进程列表当中,fork返回开始调度器调度;

    创建子进程给子进程分配对应的内核结构,理论上子进程也要有自己的代码和数据,但是一般子进程没有加载的过程,也就是说子进程没有自己的代码和数据,所以子进程只能使用父进程的代码和数据;

    代码:都是不可以被写的,只能读取,所以父子进程共享没有问题;

    数据:可能被修改,所以必须分离;

    当想要修改数据时,有两种方案

    1、创建进程时,就直接拷贝分离,也就是将父进程给子进程拷贝一份,但这样做有一个问题,可能拷贝子进程根本不会用到的数据空间,即使用到了也就是读取;

    1. #include
    2. #include
    3. int main()
    4. {
    5. char *str = "aaa";
    6. char *str1 = "aaa";
    7. printf("%p\n", str);
    8. printf("%p\n", str1);
    9. return 0;
    10. }

    此时打印出来的结果地址是相同的,编译器编译程序的时候都知道使用这种方式节省空间,所以操作系统在创建子进程的时候,不需要将不会被访问的或者只会读取的数据,拷贝出一份;那么什么样的数据值的被拷贝呢?将来会被父或子进程写入的数据;一般来说,即便是操作系统也无法提前知道哪些空间可能会被写入,并且即便是提前拷贝了可能也不会立刻使用,所以操作系统选择了写时拷贝技术将父子进程的数据进行分离;

    2、写时拷贝;

    fork之后是所有的代码共享,不是fork之后的代码共享;

    原因:首先,当代吗进行汇编后,会有很多行代码,而且每行代码加载到内存后都有对应的地址;其次,因为进程随时可能被中断(可能没有执行完),下次回来,还必须从之前的位置继续运行,就要求CPU必须随时记录下当前进程执行的位置,所以,CPU内有对应的寄存器数据,用来记录当前进程执行的位置,寄存器叫做EIP,也叫做pc(point code)指针,程序计数器;寄存器在CPU内只有一份,但是寄存器的数据,可以有很多份,而这个数据也就是上下文数据,而这个上下文数据,通过写时拷贝给子进程,父子进程各自调度,各自会修改自己的EIP,但是此时子进程已经认为自己的EIP起始值就是fork之后的代码;

    因为有写时拷贝,所以父子进程得以彻底分离,完成了进程独立性的技术保证,是一种延时申请技术,可以提高整机内存的使用率;

    fork 的常规用法

    一个父进程希望子进程复制自己,是父子进程执行同一份代码的不同区域;

    父进程和子进程执行完全不同的代码;

    一般而言父子进程实际使用时,像现实一样希望子承父业,或者父子从事不同的行业;

    fork失败的原因

    系统中有太多的进程;(内存不足)

    实际用户的进程数超过了限制;(一般普通用户的进程数是有限制的)几百几千进程;

    进程终止 

    进程终止时操作系统做了什么?

    释放进程申请的相关数据结构和对应的代码和数据;本质就是释放系统资源;内存或CPU等;

    进程终止的常见方式?

    代码跑完,结果正确;

    代码跑完,结果不正确;(代码中的逻辑有问题)

    代码没跑完,程序崩溃了;(信号部分)

    main函数的返回值?main返回值的意义是什么?return 0的含义是什么?为什么总是 0 ?

    1. #include
    2. int main()
    3. {
    4. return 0;
    5. }

     return后面的返回值并不是总是0的;

    main的返回值是进程的退出码;

    return 0 表示的是进程的运行结果正确,当运行结果不正确的时候返回值是非0;

    使用 echo $? 可以获取最近一次进程返回的退出码;

    返回值的意义是返回给上一级进程,用来评判进程执行结果用的;例;

    1. #include
    2. #include
    3. int sum(int top)
    4. {
    5. int s=0;
    6. for(int i=1;i<=top;i++)
    7. {
    8. s+=i;
    9. }
    10. return s;
    11. }
    12. int main()
    13. {
    14. int ret=0;
    15. int res=sum(100);
    16. if(res!=5050)
    17. {
    18. ret=1;//代码运行结果不正确;
    19. }
    20. return ret;
    21. }

    可以通过对返回值做判断来判断自己的程序运行结果正确还是不正确;

    如果程序结果正确了就不会关心为什么正确了,但是如果结果不正确,就会关心结果为什么不正确,非0值有无数个,不同的非0值可以表述不同的错误原因;从而给程序在运行结束后在结果不正确时,方便定位错误的原因细节;

    退出码除了返回给上一级进程,还有一个就是可以通过退出码快速定位错误原因;

    又因为自己定义的退出码系统并不能识别,所以需要用到一个接口,将退出码转化成字符串描述的错误原因; #include strerror();

    1. #include
    2. #include
    3. #include
    4. int sum(int top)
    5. {
    6. int s=0;
    7. for(int i=1;i<=top;i++)
    8. {
    9. s+=i;
    10. }
    11. return s;
    12. }
    13. int main()
    14. {
    15. for(int number=0;number<100;number++)
    16. {
    17. printf("%d: %s\n",number,strerror(number));
    18. }
    19. }

    最终打印出134个退出码和字符串;

    我们自己可以使用这些退出码和含义,但是如果想要自己定义也可以自己设计一套退出方案;如果创建一个子进程怎么才能知道它是否运行完,运行的怎么样,就需要用到退出码;

    程序崩溃的会后,退出码无意义,一般而言退出码对应的return 语句没有被执行;

    用代码如何终止进程?什么是一个正确的终止?

    1、return 语句,就是终止进程,return+退出码;注意,只有main函数中的return可以返回退出码;

    1. #include
    2. #include
    3. #include
    4. int sum(int top)
    5. {
    6. int s = 0;
    7. for(int i=1;i
    8. {
    9. s+=1;
    10. }
    11. return s;
    12. }
    13. int main()
    14. {
    15. printf("hello world1\n");
    16. printf("hello world1\n");
    17. printf("hello world1\n");
    18. int res = sum(10);
    19. return 11;
    20. printf("hello world2\n");
    21. printf("hello world2\n");
    22. printf("hello world2\n");
    23. printf("hello world2\n");
    24. }

    运行后程序的退出码是11;首先return之后,后边的代码不执行,return之后,main函数返回11,被系统拿到,进而被父进程拿到;

    2、exit函数#include    void exit(int status);

    1. #include
    2. #include
    3. #include
    4. #include
    5. int sum(int top)
    6. {
    7. int s = 0;
    8. for(int i=1;i
    9. {
    10. s+=1;
    11. }
    12. exit(222);
    13. }
    14. int main()
    15. {
    16. printf("hello world1\n");
    17. printf("hello world1\n");
    18. printf("hello world1\n");
    19. int res = sum(10);
    20. exit(111);
    21. printf("hello world2\n");
    22. printf("hello world2\n");
    23. printf("hello world2\n");
    24. printf("hello world2\n");
    25. }

    代码运行后,如果sum函数中使用return返回sum值,退出码是111,但是当sum函数中使用exit函数,可以看见退出码是222;exit函数在代码的任何地方调用都表示直接终止;

    3、_exit函数 #include      _exit(int status) 或 _Exit(int status)系统调用接口

    1. #include
    2. #include
    3. #include
    4. #include
    5. int main()
    6. {
    7. printf("can you see me?\n");
    8. sleep(3);
    9. exit(13);
    10. }

    此代码在执行时,如果将printf中的 \n 删除,将会sleep3秒后打印出里边的内容;exit函数不会直接终止,会将缓冲区中的内容打印出来后在退出;

    但是将exit函数改为_exit函数,程序会直接退出;如图;

    库函数VS系统接口

    是层状结构,上面是库函数,下面是系统接口,库函数往上走就是语言、应用等;系统接口往下走就是操作系统;所以就可见exit是库函数,而_exit是系统调用,而exit函数就是调用_exit函数;可见,缓冲区一定不是操作系统维护的,是C标准库维护的;

    进程等待

    为什么要有进程等待?

    1、子进程退出,父进程不管子进程,子进程就会进入僵尸状态,,就会导致内存泄漏;

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

    子进程完成的情况就是代码跑完,结果正确;代码跑完结果不正确;代码没跑完;

    父进程会等待子进程完成;

     进程等待的必要性

    子进程退出,父进程如果不管不顾,就可能造成 僵尸进程 的问题,进而造成内存泄漏。
    另外,进程一旦变成僵尸状态,那就刀枪不入, 杀人不眨眼 kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。
    最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。
    父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息;
     
    如何进行等待,等待是什么?(操作)
    wait函数(验证回收,回收僵尸进程的问题)
    #include

    #include

    pid_t wait(int *status); 

    1. #include
    2. int main()
    3. {
    4. pid_t id = fork();
    5. if(id<0)
    6. {
    7. perror("fork");//直接打印出错的原因
    8. exit(1);//标识进程运行完毕,结果不正确;
    9. }
    10. else if(id==0)
    11. {
    12. //子进程
    13. int cnt = 5;
    14. while(cnt)
    15. {
    16. printf("cnt:%d , 我是子进程,pid:%d , ppid: %d \n",cnt,getpid(),getppid());
    17. sleep(1);
    18. cnt--;
    19. }
    20. exit(0);//终止子进程
    21. }
    22. else
    23. {
    24. //父进程
    25. while(1)
    26. {
    27. printf("cnt:%d , 我是父进程,pid:%d , ppid: %d \n",getpid(),getppid());
    28. sleep(1);//此代码子进程退出,父进程还在运行就会导致子进程僵尸状态
    29. }
    30. }
    31. }
     此时代码运行后,子进程就会进入僵尸状态;
    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. pid_t id = fork();
    7. if(id<0)
    8. {
    9. perror("fork");//直接打印出错的原因
    10. exit(1);//标识进程运行完毕,结果不正确;
    11. }
    12. else if(id==0)
    13. {
    14. //子进程
    15. int cnt = 5;
    16. while(cnt)
    17. {
    18. printf("cnt:%d , 我是子进程,pid:%d , ppid: %d \n",cnt,getpid(),getppid());
    19. sleep(1);
    20. cnt--;
    21. }
    22. exit(0);//终止子进程
    23. }
    24. else
    25. {
    26. //父进程
    27. printf("cnt:%d , 我是父进程,pid:%d , ppid: %d \n",getpid(),getppid());
    28. sleep(7);
    29. pid_t ret=wait(NULL);//阻塞式的等待
    30. if(ret>0)
    31. {
    32. printf("等待子进程成功,ret :%d\n",ret);
    33. }
    34. }
    35. }

    运行程序后可以看到5s后子进程进入僵尸状态,再过2s后将是状态消失; 

    waitpid函数 (获取子进程退出的状态)
    #include

    #include

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

    options默认为0,表示阻塞等待;

    status:输出型参数;标识子进程退出的结果,与wait中相同;

    使用,int status=0;  waitpid(, &status,),系统会将status自动填充到status中;

    waitpid(pid , NULL ,0)等价于wait(NULL)

    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. pid_t id = fork();
    7. if(id<0)
    8. {
    9. perror("fork");//直接打印出错的原因
    10. exit(1);//标识进程运行完毕,结果不正确;
    11. }
    12. else if(id==0)
    13. {
    14. //子进程
    15. int cnt = 5;
    16. while(cnt)
    17. {
    18. printf("cnt:%d , 我是子进程,pid:%d , ppid: %d \n",cnt,getpid(),getppid());
    19. sleep(1);
    20. cnt--;
    21. }
    22. exit(0);//终止子进程
    23. }
    24. else
    25. {
    26. //父进程
    27. printf("cnt:%d , 我是父进程,pid:%d , ppid: %d \n",getpid(),getppid());
    28. sleep(7);
    29. pid_t ret=waitpid(id,NULL,0);//阻塞式的等待
    30. if(ret>0)
    31. {
    32. printf("等待子进程成功,ret :%d\n",ret);
    33. }
    34. }
    35. }

    看到现象与wait上边的代码一致;

    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. pid_t id = fork();
    7. if(id<0)
    8. {
    9. perror("fork");//直接打印出错的原因
    10. exit(1);//标识进程运行完毕,结果不正确;
    11. }
    12. else if(id==0)
    13. {
    14. //子进程
    15. int cnt = 5;
    16. while(cnt)
    17. {
    18. printf("cnt:%d , 我是子进程,pid:%d , ppid: %d \n",cnt,getpid(),getppid());
    19. sleep(1);
    20. cnt--;
    21. }
    22. exit(100);//终止子进程
    23. }
    24. else
    25. {
    26. //父进程
    27. printf("cnt:%d , 我是父进程,pid:%d , ppid: %d \n",getpid(),getppid());
    28. sleep(7);
    29. int status = 0;
    30. pid_t ret=waitpid(id,&status,0);//阻塞式的等待
    31. if(ret>0)
    32. {
    33. printf("等待子进程成功,ret :%d, status:%d\n",ret,status);
    34. }
    35. }
    36. }

    按照常理这个代码的运行结果是status将会打印出100,但是真实的运行结果却不是这个数字,status的构成,status并不是按照整数来整体使用的,而是按照比特位的方式将32个比特位划分,目前只学习低16位;次低8位表示子进程退出时的退出码,使用下面的操作,就可以得到退出码;

    1. if(ret>0)
    2. {
    3. printf("等待子进程成功,ret :%d, 子进程退出码:%d\n",ret,(status>>8)&0xFF);
    4. }

    修改后运行代码就可以得到子进程的退出码也就是上面所说的100;

    进程异常退出或者崩溃,本质是操作系统杀掉了进程,操作系统如何杀掉呢?本质是通过发送信号的方式;

    进程在退出时有31个信号,这个信号使用低7个比特位表示进程收到的信号core dump标志后边再讲;

    status & 0x7F 获取低7位; 

    父进程通过wait或者waitpid可以拿到子进程的退出结果,为什么要用waitpid函数呢?直接全局变量不行吗?

    因为进程具有独立性,那么数据就要发生写时拷贝,父进程无法拿到;

    进程具有独立性,进程退出吗不也是子进程的数据吗?父进程怎么能拿到,wait和waitpid做了什么?

    僵尸进程,至少要保留该进程的PCB信息,里边保留了子进程退出时的退出结果信息;

    本质就是读取了子进程的task_struct结构;

    wait和waitpid有权限吗?因为是系统调用,就是操作系统做的事情;本质是操作系统帮助拿子进程的退出结果;

    只有子进程退出的时候,父进程才会调用waitpid函数进行返回;

    waitpid和wait可以在目前的情况下让进程的退出具有一定的顺序性,就是子进程先退出然后父进程退出,可以让父进程进行更多的收尾工作;

    waitpid参数:id>0等待指定的进程;id==0;id==-1,等待任意一个子进程退出;

    上述代码在获取进程的退出码时还需要进行位操作,那么可不可以不适用位操作获取呢?

    系统提供了宏,它的底层就是代码中的位操作;

    将父进程中的代码改为

    1. else
    2. {
    3. int status=0;
    4. pid_t result = waitpid(id,&status,0);
    5. if(result > 0)
    6. {
    7. if(WIFEXITED(status))
    8. {
    9. printf("子进程的退出码:%d\n",WIFEXITED(status))
    10. }
    11. else
    12. {
    13. printf("子进程退出异常:%d\n",WIFEXITED(status));
    14. }
    15. }
    16. }

    如果想要让父进程在等待期间去做一些其他的事情该怎么办?

    options:默认为0,代表阻塞等待,当设置为WNOHONG是非阻塞;

    options:WNOHANG选项,代表父进程非阻塞式等待;

    Linux是C语言写的,系统调用接口是操作系统自己提供的接口,就是C语言函数,系统提供的一般大写的标记为其实就是一个宏;

    可以理解为Wait No HANG,

    一个概念:HANG住了,点开一个软件,长时间没有任何反应,就像是卡主不动了就叫做HANG住了,所谓的HANG住了就是这个进程没有被CPU调度;要么是在阻塞队列中,要么就是等待被调度;

    阻塞等待和非阻塞等待:非阻塞等待就是如父进程通过调用waitpid来进行等待,如果子进程没有退出,这个waitpid系统调用立马返回;

    进程阻塞,本质就是阻塞在系统函数的内部,在flag==0处挂起,后边的代码就不在执行了,当条件满足的时候,父进程被唤醒,是从if(flag==0 )的后面唤醒,而不是重新执行waitpid函数;阻塞等待一般就是在内核中阻塞,等待被唤醒;

    基于非阻塞调用的轮询检测方案;网络代码大部分是IO类的,不断面临阻塞与非阻塞状态;

    非阻塞等待父进程代码;

    1. else
    2. {
    3. int quit = 0;
    4. while(!quit)
    5. {
    6. int status = 0;
    7. pid_t res = waitpid(-1,&status,WNOHANG);
    8. if(res > 0)
    9. {
    10. //等待成功 && 子进程退出
    11. printf("等待子进程退出成功,退出码:%d\n"WEXITSTATUS(status));
    12. quit = 1;
    13. }
    14. else if(res == 0)
    15. {
    16. //等待成功 && 子进程未退出
    17. printf("子进程还在运行中,暂时没有退出\n");
    18. }
    19. else
    20. {
    21. //等待失败
    22. printf("wait失败\n");
    23. }
    24. }
    25. }

    此时就可以看到非阻塞等待的结果;

    进程等待的使用;

    1. #include
    2. #include
    3. #include
    4. #include
    5. typedef void (*handler_t)();//函数指针类型
    6. std::vector<handler_t> handlers;//函数指针数组;
    7. void fun_one()
    8. {
    9. printf("这是一个临时任务!\n");
    10. }
    11. void fun_two()
    12. {
    13. printf("这是一个临时任务!\n");
    14. }
    15. //设置对应的方法回调;
    16. //以后想让父进程闲了执行任何方法的时候,只要向Load里面注册,就可以让父进程执行对应的方法;
    17. void Load()
    18. {
    19. handlers,push_back(fun_one);
    20. handlers,push_back(fun_two);
    21. }
    22. int main()
    23. {
    24. pid_t id = fork();
    25. if(id==0)
    26. {
    27. int cnt = 5;
    28. while(cnt)
    29. {
    30. printf("我是子进程:%d\n",cnt--);
    31. sleep(1);
    32. }
    33. exit(1);
    34. }
    35. else
    36. {
    37. int quit=0;
    38. while(!quit)
    39. {
    40. int status = 0;
    41. pid_t res = waitpid(-1,&status,WNOHANG);
    42. if(res > 0)
    43. {
    44. //等待成功 && 子进程退出
    45. printf("等待子进程退出成功,退出码:%d\n"WEXITSTATUS(status));
    46. quit = 1;
    47. }
    48. else if(res == 0)
    49. {
    50. //等待成功 && 子进程未退出
    51. printf("子进程还在运行中,暂时没有退出\n");
    52. if(handlers,empty()) Load();
    53. for(auto iter : handlers)
    54. {
    55. //执行处理其他任务
    56. iter();
    57. }
    58. }
    59. else
    60. {
    61. //等待失败
    62. printf("wait失败\n");
    63. quit=1;
    64. }
    65. sleep(1);
    66. }
    67. }
    68. }

    如果想要让父进程在等待的时候执行什么任务就在Load函数中加入即可;

     进程程序替换

    概念+原理

    fork()之后父子各自执行父进程代码的一部分,父子代码共享,数据写时拷贝;如果子进程就想执行全新的程序怎么办?这时就可以使用进程的程序替换来完成此功能;

    程序替换是通过特定的接口,加载磁盘上的一个权限的程序(代码和数据),加载到调用进程的地址空间中;让子进程执行其他程序;

    原理

    如果想要运行other.exe程序就需要将磁盘上的程序加载到内存,并与当前进程的页表重新建立映射,这个过程可以调用系统的接口来完成;

    进程替换没有创建新的进程;

    如何理解将程序放入内存中?这个过程就叫做加载,编译有编译器,链接有链接器,而对应的加载操作也有加载器;所谓的exec系列的函数本质就是如何加载程序的函数;

    创建程序替换;

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

    在加载程序的第一步就是先找到程序,

    函数中的path 是 路径+目标文件名;

    ... 是可变参数列表,函数可以传入多个不定个数的参数;

    就像main函数的命令行参数,最后一个参数必须是NULL表示参数传递完毕;

    不创建子进程

    1. #include
    2. #include
    3. #incldue
    4. #include
    5. #include
    6. #include
    7. #include
    8. int main()
    9. {
    10. printf("当前进程的开始代码\n");
    11. execl("/usr/bin/ls","--color==auto","ls","-l",NULL);
    12. exit(1);
    13. printf("当前进程的结束代码\n");
    14. return 0;
    15. }

    运行结果在execl函数的后边就会执行命令,ls -l命令;

    execl是程序替换在运行函数成功后,会将当前进程的所有代码和数据都进行替换,包括已经执行的和没有执行的;一旦调用成功,后续所有的代码全都不会执行;

    如果出错了就会返回-1;

    为什么调用成功没有返回值?因为成功了就会将包括函数自身的代码都会被替换,所以就没有对应的返回值;

    所以在execl函数后边接exit(1);

    "--color==auto"会使输出的字符带颜色;

    例如C++程序怎么与python或java语言所写的函数耦合,关键点就在这里;

    创建子进程

    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. pid_t id = fork();
    7. if(id == 0)
    8. {
    9. //子进程
    10. //让子进程执行命令ls -a -l
    11. printf("子进程开始运行,pid:%d\n",getpid());
    12. execl("/use/bin/ls","ls","-a","-l",NULL);
    13. exit(1);
    14. }
    15. else
    16. {
    17. //父进程
    18. printf("父进程开始运行,pid:%d\n",getpid());
    19. int status = 0;
    20. pid_t id = waitpid(-1,&status,0);//阻塞等待;
    21. if(id>0)
    22. {
    23. printf("wait success,exit code:%d\n",WEXITSTATSU(status));
    24. }
    25. }
    26. return 0;
    27. }

    运行结果如图

    为什么要创建子进程?

    如果不创建子进程,那么退换的程序只能是父进程的,如果创建了子进程,那么替换的程序就是子进程,而不影响父进程;

    为了不影响父进程;因为想让父进程聚焦在读取数据,解析数据,指派进程执行代码的功能;

    在记载新程序之前,父子进程的代码和数据的关系是代码共享,数据写时拷贝;

    当子进程加载新的程序的时候,就是一种写入;代码也会发生写时拷贝,也就是将父子的代码分离;此时父子进程的代码和数据就会彻底分离开;

    execv

    其中v可以看做是vector;

    与execl只有传参方式的区别;

    1. #include
    2. #include
    3. #include
    4. #define NUM 16
    5. int main()
    6. {
    7. pid_t id = fork();
    8. if(id == 0)
    9. {
    10. //子进程
    11. //让子进程执行命令ls -a -l
    12. printf("子进程开始运行,pid:%d\n",getpid());
    13. char *const _argv[NUM] =
    14. {
    15. (char*)"ls",
    16. (char*)"-a",
    17. (char*)"-l",
    18. NULL
    19. };
    20. execv("/usr/bin/ls",_argv);
    21. exit(1);
    22. }
    23. else
    24. {
    25. //父进程
    26. printf("父进程开始运行,pid:%d\n",getpid());
    27. int status = 0;
    28. pid_t id = waitpid(-1,&status,0);//阻塞等待;
    29. if(id>0)
    30. {
    31. printf("wait success,exit code:%d\n",WEXITSTATSU(status));
    32. }
    33. }
    34. return 0;
    35. }

    execlp

    要执行程序先找到程序;带路径才能找到,不带路径可以找到程序吗?

    通过PATH环境变量可以找到程序,

    此函数就是会自己在环境变量中查找,不用传入路径;

    execlp("ls","ls","-a","-l",NULL);

    前边的ls表示要找的程序,后边的ls表示要怎么执行这个程序;

    execvp

    与execlp函数的不同点就是传参方式不同;

    execvp("ls",_argv);

    execle

    如何执行其他自己写的C++、二进制程序;

    1. #include
    2. #include
    3. #include
    4. int main(int argc,char *argv[])
    5. {
    6. if(argc != 2)
    7. {
    8. printf("can not execute!\n");
    9. exit(1);
    10. }
    11. if(strcmp(argv[1],"-a") == 0)
    12. {
    13. printf("hello a!\n");
    14. }
    15. else if(strcmo(argv[1],"-b") == 0)
    16. {
    17. printf("hello b!\n");
    18. }
    19. else
    20. {
    21. printf("default!\n");
    22. }
    23. return 0;
    24. }
    1. #include
    2. #include
    3. #include
    4. #define NUM 16
    5. const char *myfile = "/zzz/111/222/myfile1";
    6. //若此处使用路径 "./myfile" 也可以正常执行;
    7. int main()
    8. {
    9. pid_t id = fork();
    10. if(id == 0)
    11. {
    12. //子进程
    13. //让子进程执行命令ls -a -l
    14. printf("子进程开始运行,pid:%d\n",getpid());
    15. execl(myfile,"myfile1","-a",NULL);
    16. exit(1);
    17. }
    18. else
    19. {
    20. //父进程
    21. printf("父进程开始运行,pid:%d\n",getpid());
    22. int status = 0;
    23. pid_t id = waitpid(-1,&status,0);//阻塞等待;
    24. if(id>0)
    25. {
    26. printf("wait success,exit code:%d\n",WEXITSTATSU(status));
    27. }
    28. }
    29. return 0;
    30. }

    如何执行其他语言的程序;

     test.py

    1. #! /usr/bin/python3.6
    2. print("hello Python\n");

    test.sh

    1. #! /usr/bin/bash
    2. echo "hello shell!"

    execlp函数的使用

    execlp("python","python","test.py",NULL);

    当Python加上可执行权限时

    execlp("./test.py","test.py"NULL);

    exec系列的函数其实就是加载器的底层接口;

    execle函数

    使用

    execle(myfile,"myfile","-a",NULL,);

    首先获取环境变量

    myfile1.c

    1. #include
    2. #include
    3. #include
    4. int main(int argc,char *argv[])
    5. {
    6. if(argc != 2)
    7. {
    8. printf("can not execute!\n");
    9. exit(1);
    10. }
    11. //MY_VAL:目前此环境变量并不存在;
    12. printf("获取环境变量:MY_VAL: %s\n",getenv("MY_VAL"));
    13. if(strcmp(argv[1],"-a") == 0)
    14. {
    15. printf("hello a!\n");
    16. }
    17. else if(strcmo(argv[1],"-b") == 0)
    18. {
    19. printf("hello b!\n");
    20. }
    21. else
    22. {
    23. printf("default!\n");
    24. }
    25. return 0;
    26. }

    exec.c

    1. #include
    2. #include
    3. #include
    4. #define NUM 16
    5. const char *myfile = "/zzz/111/222/myfile1";
    6. //若此处使用路径 "./myfile" 也可以正常执行;
    7. int main()
    8. {
    9. char *_env[NUM]=
    10. {
    11. (char*)"MY_VAL = 8877",//环境变量;
    12. NULL
    13. }
    14. pid_t id = fork();
    15. if(id == 0)
    16. {
    17. //子进程
    18. //让子进程执行命令ls -a -l
    19. printf("子进程开始运行,pid:%d\n",getpid());
    20. execle(myfile,"myfile1","-a",NULL,_env);
    21. exit(1);
    22. }
    23. else
    24. {
    25. //父进程
    26. printf("父进程开始运行,pid:%d\n",getpid());
    27. int status = 0;
    28. pid_t id = waitpid(-1,&status,0);//阻塞等待;
    29. if(id>0)
    30. {
    31. printf("wait success,exit code:%d\n",WEXITSTATSU(status));
    32. }
    33. }
    34. return 0;
    35. }

    此时就会将_env的环境变量传到myfile1中;

     execve函数是真正的系统调用函数,其他的几个函数都是间接的使用这个函数,是系统提供的基本封装,为了满足上层的不同应用场景;

    简易的shell实现 

    myshell.c

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #define NUM 1024
    8. #define SIZE 32
    9. #define SEP " "
    10. //保存打散之后的命令行字符串;
    11. char *g_argv[SIZE];
    12. //保存完整的命令行字符串;
    13. char cmd_line[NUM];//定义一个大小是1024的缓冲区;
    14. //shell 运行原理通过让子进程执行命令,父进程等待并且解析命令
    15. //除了要执行系统的命令还要执行自己的命令,如果自己的命令崩溃那么不会影响父进程,使得父进程能够继续解析命令;
    16. int main()
    17. {
    18. //0、命令行解释器一定是一个常驻内存的进程,就是不退出;
    19. while (1)
    20. {
    21. //1、打印出提示信息
    22. //[zyl@123456host myshell]
    23. printf("[root@123456host myshell]# \n");//后边需要加\n ,否则数据在缓冲区内;
    24. fflush(stdout);//在输入命令时总是换行,与printf里的内容不在一行;
    25. memset(cmd_line, '\0', sizeof cmd_line);//sizeof可以不带圆括号;
    26. //2、获取用户的输入,输入的是各种命令和选项:ls -a -l -i
    27. if (fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
    28. {
    29. continue;
    30. }
    31. cmd_line[strlen(cmd_line) - 1] = '\0';//由于在输入时输入了回车,所以在打印的时候也会打印处一行空行,
    32. //于是通过上边的这行代码将 \n 置为0;
    33. printf("echo: %s\n", cmd_line);
    34. //3、命令行字符串解析:将输入的命令 "ls -a -l -i" 转换成 "ls" "-a" "-l" "-i"
    35. g_argv[0] = strtok(cmd_line, SEP);//第一次调用,要传入原始字符
    36. int index = 1;
    37. if (strcmp(g_argv[0], "ls") == 0)//使得ls命令行带有颜色
    38. {
    39. g_argv[index++] = "--color=auto";
    40. }
    41. //while 先调用函数,然后赋值给g_argv,然后while检测g_argv中的值;
    42. while (g_argv[index] = strtok(NULL, SEP));//第二次,如果还要解析原始字符串,传入NULL;
    43. for (index = 0; g_argv[index]; index++)
    44. {
    45. printf("g_argv[%d]: %s\n", index, g_argv[index]);
    46. }
    47. //4、内置命令处理:让父进程自己执行的额命令,叫做内置命令(内建命令);
    48. //本质就是shell中的函数调用;
    49. //如果没有第四步,那么执行任何命令都会运行execvp,那么在cd .. 的时候就会出现路径不发生变化的现象
    50. //只会使子进程的路径发生变化,已运行完就会退出;但是pwb打印出来的相当于是父进程的路径没有改变;
    51. if (strcmp(g_argv[0], "cd") == 0)//不想让子进程执行,希望使父进程执行命令,
    52. {
    53. //chdir函数更改当前的路径
    54. if (g_argv[1] != NULL)
    55. {
    56. chdir(g_argv[1]);
    57. }
    58. continue;
    59. }
    60. //5、fork()
    61. pid_t id = fork();
    62. if (id == 0)
    63. {
    64. //child
    65. printf("下面的功能让子进程进行!\n");
    66. execvp(g_argv[0],g_argv);
    67. exit(1);
    68. }
    69. int status = 0;
    70. pid_t ret = waitpid(id, &status, 0);
    71. if (ret > 0)
    72. {
    73. printf("exit code: %d\n", WEXITSTATUS(status));
    74. }
    75. }
    76. //
    77. return 0;
    78. }

    为什么要进行程序替换?

    和应用场景有关,在一些场景下需要对程序进行替换;

     

    shell执行的命令通常有两种:

    1、第三方提供的对应的在磁盘中有具体的二进制文件的可执行程序(由子进程执行);

    2、shell内部,自己实现的方法,由自己(父进程)来执行;因为有些命令要影响shell本身的cd,export;

  • 相关阅读:
    基于MUSIC算法的二维超声波成像matlab仿真
    【Java技术路线】7. 异常与调试
    已解决AttributeError: ‘function‘ object has no attribute ‘ELement‘
    TwinCAT3加不上路由ADS的几种可能
    12-JavaSE基础巩固练习:字符串练习
    Spring系列15:Environment抽象
    Android 13.0 recovery出厂时清理中字体大小的修改
    音视频基础知识
    stm32f103开发板入门到手进行开发
    合并两个排序的链表
  • 原文地址:https://blog.csdn.net/zzzylo/article/details/133869752