• 【Linux】进程创建/终止/等待/替换



    需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。


     目录

    一、子进程的创建

    1、fork函数的概念

    2、如何理解fork拥有两个返回值

    3、fork调用失败的场景

    二、进程的终止

    1、main函数返回值

    1.1main函数的返回值的意义

    1.2将错误码转化为错误信息

    1.3查看进程的退出码

    2、进程退出的情况

    1、进程的正常退出与异常退出

    2、库函数exit

    3、系统调用_exit

    三、进程等待

    1、进程等待的必要性

    2、进程等待的方法

    2.1系统调用wait

    2.2系统调用waitpid

    3、status值的意义

    4、图解父进程等待子进程的方式

    5、阻塞/非阻塞式等待

    四、进程程序替换

    1、进程程序替换的概念

    2、进程程序替换的原理

    3、exec*()系列函数的返回值

    4、main函数在磁盘中被加载到内存的原理

    五、写一个shell


    一、子进程的创建

    1、fork函数的概念

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

    1. #include
    2. pid_t fork(void);

    返回值:fork创建子进程成功后,会给父进程返回子进程的PID,给子进程返回0,失败则返回-1。

    为什么要让父进程拿到子进程的PID?因为子进程变为僵尸状态后,需要父进程读取子进程的退出信息并回收资源。

    操作系统将会给创建成功的子进程:

    1、给子进程分配新的内存块和内核数据结构(PCB、进程地址空间、页表等,并构建对应的映射关系);

    2、将父进程的部分数据结构内容拷贝至父进程;

    3、把子进程添加到系统进程列表中;

    4、fork返回,调度器开始调度。

    2、如何理解fork拥有两个返回值

    fork函数return之前,就已经有了父子两个进程,给父进程返回子进程的PID,给子进程返回0,失败则返回-1。

    利用这个特性,我们可以用变量接收返回值,根据fork返回值不同让父子进程执行不同的代码。

    1. #include
    2. #include
    3. int main()
    4. {
    5. pid_t id=fork();
    6. if(id==0)
    7. {
    8. printf("子进程:pid=%d,ppid=%d | grobal_val=%d,&grobal_val=%p\n",getpid(),getppid(),grobal_val,&grobal_val);
    9. }
    10. else if(id>0)
    11. {
    12. printf("父进程:pid=%d,ppid=%d | grobal_val=%d,&grobal_val=%p\n",getpid(),getppid(),grobal_val,&grobal_val);
    13. sleep(1);
    14. }
    15. else
    16. {
    17. printf("fork error\n");
    18. return 1;
    19. }
    20. return 0;
    21. }

    pid_t id=fork()这句代码父子进程谁先返回不确定。谁先返回,谁就在虚拟内存中写入id的值,后返回的进程由于进程的独立性将会发生写时拷贝。所以,我们可以看到父子进程的id变量的虚拟地址是一样的,但是内容却不一样。

    3、fork调用失败的场景

    系统中的进程数达到了最大限制。

    二、进程的终止

    1、main函数返回值

    1.1main函数的返回值的意义

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

    return 后面的值代表进程退出的时候,对应的退出码。标定进程执行的结果是否正确。

    如果不关心一个进程的退出码,可以直接return 0;如果要关心进程的退出码,则要返回特定的数字以表明进程不同的错误。

    1.2将错误码转化为错误信息

    函数原型:

    1. #include
    2. char* strerror(int errnum);

    遍历打印错误码: 

    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. for(int i=0;i<150;++i)
    7. {
    8. printf("num[%d]:%s\n",i,strerror(i));
    9. }
    10. return 0;
    11. }

    可以看到不同的错误码对应的出错误信息。

    1.3查看进程的退出码

    echo $?

    $?会记录最近一个进程在命令行中执行完毕时的退出码,即main函数的返回值。

    2、进程退出的情况

    1、进程的正常退出与异常退出

    正常终止(父进程获取退出码):

    1、程序执行完毕,main函数返回0

    2、程序执行完毕,但是返回值不为0,例如调用exit()和_exit()或return !0。

    异常终止(父进程获取退出信号):

    ctrl+c或除0错误等导致程序被信号终止。

    2、库函数exit

    函数原型:

    1. #include
    2. void exit(int status);

    库函数exit在进程退出后,会主动刷新缓冲区。

    3、系统调用_exit

    函数原型:

    1. #include
    2. void _exit(int status);

    系统调用_exit在进程退出后,并不会主动刷新缓冲区。

    三、进程等待

    1、进程等待的必要性

    子进程退出后会进入僵尸状态,父进程通过进程等待的方式,获取子进程的退出信息,回收子进程资源,让子进程结束僵尸状态。当然,在子进程没有退出时,父进程只能阻塞等待子进程变成僵尸状态。

    2、进程等待的方法

    头文件:

    1. #include
    2. #include

    2.1系统调用wait

    函数原型:

    pid_t wait(int* status);

    返回值:等待成功被等待进程的pid,失败返回-1。

    参数:status输出型参数,获取子进程退出码和退出状态,不关心则可以设置成为NULL。

    通过wait让父进程获取子进程的PID。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main()
    7. {
    8. pid_t id=fork();
    9. if(id==0)
    10. {
    11. //子进程
    12. int cnt=3;
    13. while(cnt--)
    14. {
    15. printf("子进程:%d 父进程:%d %d\n",getpid(),getppid(),cnt);
    16. sleep(1);
    17. }
    18. exit(0);//子进程退出
    19. }
    20. sleep(5);
    21. pid_t ret =wait(NULL);
    22. if(id>0)
    23. {
    24. //父进程
    25. printf("等待成功:%d\n",ret);
    26. }
    27. return 0;
    28. }

    2.2系统调用waitpid

    函数原型:

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

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

    参数:

    PID:

    PID=-1,等待任一个子进程。与wait等效。PID>0,等待进程为PID的子进程。

    status:

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

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

    1. //WIFEXITED和WEXITSTATUS是两个宏
    2. if(WIFEXITED(status))//判断子进程是否正常退出(判断退出信号)
    3. {
    4. printf("exit code:%d\n",WEXITSTATUS(status));//判断子进程运行结果是否正常(判断退出码)
    5. }
    6. else{
    7. //something to do
    8. }

    options:

    WNOHANG: 若PID指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的PID。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main()
    7. {
    8. pid_t id=fork();
    9. if(id==0)
    10. {
    11. //子进程
    12. int cnt=3;
    13. while(cnt--)
    14. {
    15. printf("子进程:%d 父进程:%d %d\n",getpid(),getppid(),cnt);
    16. sleep(1);
    17. }
    18. exit(10);//子进程退出
    19. }
    20. int status=0;
    21. pid_t ret =waitpid(id,&status,0);
    22. if(id>0)
    23. {
    24. //父进程
    25. printf("等待成功:%d,ret=%d\n",ret,status);
    26. }
    27. sleep(5);
    28. return 0;
    29. }

    通过修改子进程的退出码,可以打印出不同的status值,因为status用于存储子进程的退出码和退出信号。

    3、status值的意义

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

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

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

    status不能简单的当作整型,可以当作位图来看待。(只研究status低16比特位):

    在进程退出时,终止信号是评判一个进程是否正常退出;退出状态是评判一个进程运行的结果是否正确。

    1、使用kill -l来查看进程终止的信号。

    2、根据退出码确定进程的退出状态。

    以下代码手动调用waitpid模拟父进程等待子进程的过程:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main()
    7. {
    8. pid_t id=fork();
    9. if(id==0)
    10. {
    11. //子进程
    12. int cnt=3;
    13. while(cnt--)
    14. {
    15. printf("子进程:%d 父进程:%d %d\n",getpid(),getppid(),cnt);
    16. sleep(1);
    17. }
    18. exit(10);//子进程退出
    19. }
    20. int status=0;
    21. pid_t ret =waitpid(id,&status,0);
    22. if(id>0)
    23. {
    24. //父进程
    25. printf("等待成功,返回值子进程的PID:%d,终止信号:%d,子进程退出码:%d\n",ret,(status&0X7F),((status>>8)&0XFF));
    26. }
    27. sleep(5);
    28. return 0;
    29. }

    4、图解父进程等待子进程的方式

    1、调用wait/waitpid后,父进程只能阻塞等待子进程变成僵尸状态。

    2、子进程退出后变成僵尸状态,task_struct中的代码和数据会被释放掉,并把自己的退出信号、退出码写入到自己的task_struct中;

    3、wait/waitpid是系统调用,操作系统有资格也有能力去读取子进程的task_struct。

    4、父进程通过进程等待的方式,获取子进程的退出信息,回收子进程资源,让子进程结束僵尸状态。

    5、阻塞/非阻塞式等待

    阻塞式等待:当父进程调用wait/waitpid(第三个参数为0)等待子进程,如果子进程暂未退出,父进程会被阻塞,暂停运行,如果父进程刚好没事干,可以选择使用阻塞等待。

    非阻塞式等待:当父进程调用waitpid(第三个参数为WNOHANG)等待子进程,如果父进程检测到子进程未退出,父进程并不会原地等待,而是继续执行自己的代码。如果使用while循环,便能达到轮询的效果。

    非阻塞式等待不会占用父进程的精力,父进程可以在轮询的过程中做其他事情:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #define NUM 5
    9. typedef void (*func_t)(); //函数指针
    10. func_t handlerTask[NUM];//函数指针数组
    11. //任务
    12. void task1()
    13. {
    14. printf("handler task1\n");
    15. }
    16. void task2()
    17. {
    18. printf("handler task2\n");
    19. }
    20. void task3()
    21. {
    22. printf("handler task3\n");
    23. }
    24. void loadTask()
    25. {
    26. memset(handlerTask, 0, sizeof(handlerTask));//将函数指针数组初始化为0
    27. handlerTask[0] = task1;//函数指针数组handlerTask[0]存放task1的地址
    28. handlerTask[1] = task2;
    29. handlerTask[2] = task3;
    30. }
    31. int main()
    32. {
    33. pid_t id = fork();
    34. assert(id != -1);
    35. if(id == 0)
    36. {
    37. //child
    38. int cnt = 10;
    39. while(cnt)
    40. {
    41. printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
    42. sleep(1);
    43. }
    44. exit(10);
    45. }
    46. loadTask();//加载任务
    47. // parent
    48. int status = 0;
    49. while(1)//父进程对子进程状态轮询
    50. {
    51. pid_t ret = waitpid(id, &status, WNOHANG);//第三个参数为0表示阻塞式等待,为WNOHANG表示非阻塞式等待
    52. if(ret == 0)//等于0代表没有被等待的进程暂未退出
    53. {
    54. printf("wait done, but child is running...., parent running other things\n");
    55. for(int i = 0; handlerTask[i] != NULL; i++)//遍历到NULL,即0停止
    56. {
    57. handlerTask[i](); //采用回调的方式,执行我们想让父进程在空闲的时候做的事情
    58. }
    59. }
    60. else if(ret > 0)//waitpid调用成功,并且子进程退出,返回值为被等待子进程的pid
    61. {
    62. printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
    63. break;
    64. }
    65. else//等于-1表示等待失败,waitpid中的第一个参数值传错会导致调用失败
    66. {
    67. printf("waitpid call failed\n");
    68. break;
    69. }
    70. sleep(1);
    71. }
    72. return 0;
    73. }

    四、进程程序替换

    1、进程程序替换的概念

    将磁盘中指定的程序加载到内存中,让指定的进程进行执行。不论是何种后端语言写的程序,exec*函数都可以调用。

    替换函数:

    1. #include `
    2. //execve的封装
    3. int execl(const char *path, const char *arg, ...);
    4. int execlp(const char *file, const char *arg, ...);
    5. int execle(const char *path, const char *arg, ...,char *const envp[]);
    6. int execv(const char *path, char *const argv[]);
    7. int execvp(const char *file, char *const argv[]);
    8. int execvpe(const char *file, char *const argv[],char *const envp[]);
    9. //系统调用
    10. int execve(const char *filename, char *const argv[],char *const envp[]);

    l(list) : 表示参数采用列表 ;

    p(path) : 带p,不用传入地址,传入可执行程序的名字即可,它会自动去环境变量PATH中寻找该可执行程序的地址;

    v(vector) :执行参数放入数组中,统一传递;

    e(env) : 可以传入自己写的环境变量。

    1、通过execl函数调用ls命令:

    1. #include
    2. #include
    3. int main()
    4. {
    5. printf("process is running·····\n");
    6. execl("/usr/bin/ls"/*要执行的程序*/,"ls","--color=auto","-a","-l",NULL/*如何执行*/);//一定要用NULL结尾
    7. printf("process is down·····\n");//这句话并不会被打印,因为后续代码和数据已经被execl函数替换了
    8. return 0;
    9. }

    如果调用execl函数,参数传错导致函数调用失败,后续代码将不会被覆盖。

    2、通过execle函数调用外部程序并使用自定义环境变量:

    1. #include
    2. #include
    3. int main()
    4. {
    5. printf("process is running··\n");
    6. //使用自定义的环境变量
    7. //char* const _env[]={(char*)"MYENV=12345",NULL};
    8. //execle("./mybin","mybin",NULL,_env);
    9. //使用系统的环境变量
    10. //extern char** environ;
    11. //execle("./mybin","mybin",NULL,environ);//系统环境变量不传,进程也能获取
    12. //使用putenv将自己的环境变量导入到environ指向的环境变量表中
    13. extern char** environ;
    14. char* const _env[]={(char*)"MYENV=12345",NULL};
    15. putenv((char*)"MYENV=654321");
    16. execle("./mybin","mybin",NULL,environ);
    17. printf("process is running··\n");
    18. return 0;
    19. }

    2、进程程序替换的原理

    程序替换的本质:用磁盘指定位置上的程序的代码和数据,覆盖进程自身的代码和数据,达到让进程执行指定程序的目的。

    3、exec*()系列函数的返回值

    exec()函数仅在发生错误时返回。返回值为-1,设置errno以指示错误。

    可以看到exec*系列函数调用成功后并没有返回值,因为exec*一旦被调用成功,后续代码将被覆盖,根本没机会用到返回值。

    4、main函数在磁盘中被加载到内存的原理

    五、写一个shell

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #define NUM 100
    9. #define OPT_NUM 20
    10. char LineCommand[NUM];
    11. char* myargv[OPT_NUM];//指针数组,用于记录切割出来的字符串
    12. int lastCode = 0;
    13. int lastSig = 0;
    14. int main()
    15. {
    16. while(1)
    17. {
    18. printf("用户名@主机名 当前路径:");
    19. fflush(stdout);
    20. //获取输入内容
    21. char* s=fgets(LineCommand,sizeof(LineCommand),stdin);
    22. assert(s!=NULL);
    23. //清除最后一个\n
    24. LineCommand[strlen(LineCommand)-1]=0;
    25. //字符串切割
    26. myargv[0]=strtok(s," ");
    27. int i=1;
    28. if(myargv[0]!=NULL&&strcmp(myargv[0],"ls")==0)//判断myargv[0]是否切割正常,穷举指令,为指令添加选项
    29. {
    30. myargv[i++]=(char*)"--color=auto";
    31. }
    32. while(myargv[i++]=strtok(NULL," "));//如果切割完毕,strtok会返回NULL,刚好myargv[end]需要等于NULL
    33. if(myargv[0]!=NULL&&strcmp(myargv[0],"cd")==0)//解决cd命令返回的是子进程的上级目录问题
    34. {
    35. if(myargv[1]!=NULL)
    36. {
    37. chdir(myargv[1]);//使用chdir()改变父进程的当前路径
    38. continue;//如果改变路径,就continue跳出本轮循环,后续子进程可以不用创建
    39. }
    40. }
    41. if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
    42. {
    43. if(strcmp(myargv[1], "$?") == 0)
    44. {
    45. printf("%d, %d\n", lastCode, lastSig);
    46. }
    47. else
    48. {
    49. printf("%s\n", myargv[1]);
    50. }
    51. continue;
    52. }
    53. //测试是否成功
    54. #ifdef DEBUG
    55. for(i=0;myargv[i];++i)
    56. {
    57. printf("myargv[%d]:%s\n",i,myargv[i]);
    58. }
    59. #endif
    60. //执行命令
    61. pid_t id=fork();//创建子进程,让子进程去执行被切割出来的指令
    62. assert(id!=-1);
    63. if(id==0)
    64. {
    65. execvp(myargv[0],myargv);
    66. exit(1);
    67. }
    68. int status = 0;
    69. pid_t ret = waitpid(id, &status, 0);
    70. assert(ret > 0);
    71. (void)ret;
    72. lastCode = ((status>>8) & 0xFF);
    73. lastSig = (status & 0x7F);
    74. }
    75. return 0;
    76. }

    运行该程序,可以实现shell的效果,如果33行-40行逻辑如果不写,使用cd ..指令回到上级目录时,发现我们在原地TP。

    原因:因为这个"模拟shell"程序会使用fork()创建子进程执行指令,当使用cd ..时,回到的是子进程的上级目录,不会改变父进程的当前工作目录。可以额外判断一下cd命令,如果是cd命令,使用chdir改变父进程的工作目录,continue跳出本轮循环。(无需创建后续子进程)

    像这种不需要子进程来执行,而是让shell来执行的命令叫做内建/内置命令这也解释了为什么echo能打印本地环境变量,因为echo也是一个内建命令,由shell执行,shell当然能打印本地环境变量。

    查看进程当前目录:

    可以使用chdir()更改进程的当前目录。

  • 相关阅读:
    面试心经
    音视频封装格式
    amber14自由能计算及增强采样方法
    计算机毕业设计(附源码)python智能垃圾分类系统
    微信小程序开发18 持续在线:如何借助云应用持续运行队列消费者?
    java绘制心形爱心
    Java调用第三方库JNA(C/C++)
    Mybatis详解
    AcWing算法基础课笔记 1.基础算法
    C++回顾<二>:类-this指针-构造函数-析构函数-隐式/显式调用explicit-初始化列表 -static静态成员变量/函数-常对象|常函数
  • 原文地址:https://blog.csdn.net/gfdxx/article/details/128046685