Linux操作系统: 进程控制

本文详细探讨了Linux操作系统中的进程控制,包括fork创建子进程的原理,如写时拷贝技术确保父子进程数据独立;进程终止时操作系统的处理,以及进程退出码的意义;使用wait和waitpid进行进程等待的原因和方式;最后介绍了进程替换的概念,通过exec系列函数实现进程的代码和数据替换。通过对这些概念的深入理解,读者能更好地掌握Linux进程管理的精髓。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

进程创建!

fork创建子进程做了什么

父子进程共享一份数据的原因:

父子进程分离数据的方式:写时拷贝

有关程序计数器和EIP寄存器:

fork常规用法 + fork失败的原因

用法:

失败原因:

进程终止!

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

进程终止的分类情况

进程退出码

退出码的概念

C/C++程序中的退出码

用代码如何终止一个进程

exit vs _exit

进程等待!

需要进行进程等待的原因:

进程等待的方式:

wait

waitpid

wait/waitpid总结:

进程替换!

进程替换的概念和原理:

exec系列,进程替换接口函数:

函数介绍:

测试代码:

 综合进程创建,终止,等待,转换写的一个简易shell:


进程创建!

正如我们所知,fork用于创建子进程。事实上,源程序通过编译为二进程可执行程序,再加载到内存中成为进程。我们可以理解为fork就是在进程执行过程中,通过执行系统调用中的指令的方式再创建一个子进程。

至于创建子进程的过程,以及创建之后的细节正是我们在进程创建部分需要关注的。


fork创建子进程做了什么

进程 = 内核数据结构 + 进程的代码和数据

=》 内核数据结构是操作系统创建的。而进程的代码和数据,一般是从磁盘中加载到内存而来。

=》 fork创建子进程,由操作系统为子进程创建对应的内核数据结构:PCB,进程地址空间对象,页表等。虽然子进程内核数据结构中的大部分数据是从父进程那里拷贝来的,但是因为进程的独立性,这部分内核数据结构确实是子进程独有的。

       而子进程对应的代码和数据,并不是从磁盘中加载而来。而是直接和父进程”共享一份代码和数据“,可是,基于进程的独立性,父子进程共享一份代码和数据是如何实现的呢?

=》 对于代码:代码具有只读属性,也就是父子进程都不会修改,那么共享一份代码是完全可以的,同时这样还节省了内存空间

=》 对于数据:事实上,子进程在创建初期,确实和父进程共享一份数据。

父子进程共享一份数据的原因:

基于进程独立性,本身,父子进程的数据必须分离。那么在子进程创建初期,直接在内存中拷贝一份父进程的数据是完全可以的。 但是这样有很大的内存浪费的风险。

=》 父进程的数据,并非所有子进程都需要使用。  即使需要使用,也不会立刻使用。  即使立刻使用,也不一定是写操作。

=》 基于高效和节省内存空间的理念,我们不能采取直接拷贝一份父进程的数据。

=》 故我们在子进程创建时,是和父进程共享同一份数据。

=》 对于只读的,父子进程共享数据完全可以。对于父进程写的,或者子进程写的数据。基于进程独立性,必须进行数据分离。 操作系统采用的即 写时拷贝技术,来将父子进程数据分离

父子进程分离数据的方式:写时拷贝

写时拷贝,在fork创建子进程这里的具体操作是:创建子进程之后,当父子进程的任何一方以写方式访问某个数据时,操作系统在内存的其他区域拷贝一份这个数据,并修改写此数据的那个进程的页表映射关系,使父子进程访问的是物理内存中的不同数据。以达到父子进程数据分离的效果,避免互相干扰。

写时拷贝好处:

在对数据进行写入时,再拷贝数据,是高效使用内存的一种表现,提高内存使用率,可以提高整机的运行效率。

写时拷贝还是一种延时申请的技术。提高内存使用率。

有关程序计数器和EIP寄存器:

一个事实:在fork创建子进程之后,父子进程的代码是共享的。那么为什么子进程是从fork内部的return或者说fork之后的代码开始执行呢?

我们知道,子进程的创建是以父进程为模板的,比如大部分内核数据结构,代码,数据等。包括进程地址空间,页表。都是从父进程那里拷贝过来的。

=》 再基于一个事实:进程中的代码指令,每条代码指令都有地址,在CPU内部的寄存器中,有一个名为EIP的寄存器,称为程序计数器,Program Counter,简称PC。用于存放下一条指令所在单元的地址。而CPU执行一个进程的代码指令就是依靠程序计数器。大致过程为:取指令(依据PC),分析指令(指令集架构ISA),执行指令。

=》 同时,进程并非一次就将全部的指令都执行完,而是执行若干时长,就需要切换为其他进程。切换时,之前的进程需要保存好程序计数器中的数据和其他相关数据。称为进程的上下文数据。以便下次切换为此进程时,可以继续上次CPU调用的指令。

=》 而fork创建子进程时,子进程的上下文数据都是从父进程那里拷贝过来的。所以,当子进程执行时,程序计数器中保存的指令地址致使子进程从父进程执行fork那里开始执行。同时fork之前的代码子进程是可见的,只是不是从头开始执行。

fork常规用法 + fork失败的原因

用法:

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

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

失败原因:

1. 系统中有太多的进程

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

3. 内存不足?

进程终止!

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

释放进程相关内核数据结构和对应的代码和数据。 即释放进程占用的资源。

进程终止的分类情况

1.1 代码跑完,运行结果正确(符合预期)

1.2 代码跑完,运行结果不正确(不符合预期)

2.   代码没有跑完,程序异常退出(崩溃)

进程退出码

退出码的概念

1. 进程退出码,是当进程执行结束后,用于标识运行结果的。

2. 通常情况下,0标识success,非0标识结果不正确。

3. 非零值有无数个,用不同的非零值来标识不同的进程错误原因。所以,依据进程退出码,可以判断上方情况的1.1 和 1.2

4. 当进程异常退出时(崩溃),是由操作系统发送信号给进程以实现的。此时,退出码没有意义。

5. 进程的退出码是返回给上一级进程的,用于上一级进程评判该进程执行结果用的。(如果上一级进程不关心,则可以忽略)

6. C语言中定义了一套将退出码/错误码转化为字符串描述的方案,   你可以自己设定一套自己的。strerror库函数可以查询C语言规定的退出码对应的退出原因。

7. Linux下,echo $?可查询上一个进程执行的退出码

C/C++程序中的退出码

其中C/C++程序中,main函数的return值就是该进程(程序)的退出码,我们通常写的return 0即表示当main函数运行结束时,退出码为0标识结果成功。

用代码如何终止一个进程

1. main函数中的return语句,即标识终止此进程。(其余函数的return仅表示此函数的结束,或者返回一个值给该函数的函数调用处)

2. void exit(int status);    C语言库函数,返回status作为退出码,并终止此进程

3. void _exit(int status);    操作系统系统接口函数,返回status作为退出码,并终止此进程

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

exit vs _exit

一个是C语言库函数,一个是操作系统接口函数。

区别:exit在程序退出之前,会刷新C标准库设定的缓冲区。执行用户通过 atexit或on_exit定义的清理函数。关闭所有打开的流,所有的缓存数据均被写入。 最后调用_exit!

进程等待!

需要进行进程等待的原因:

一、子进程退出之后,父进程不读取子进程的退出状态,不回收其资源。子进程变为僵尸进程。则子进程会造成内存泄漏。 二、 除了回收子进程的资源,如果父进程想读取子进程的退出码。也需要通过进程等待的方式获取。(子进程的运行结果正确与否)

总之:父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

进程等待的方式:

wait

       pid_t wait(int *status);

返回值:
       wait成功,子进程结束,则返回等待的子进程的退出码(阻塞式等待)
       wait不成功,返回-1(wait调用失败)
参数:
       status为输出型参数,用于获取子进程的退出信息。不关心则可以设置为NULL

注:
    wait只能阻塞式等待:父进程阻塞式等待子进程结束,等待wait函数返回。

一段非常简单的wait测试代码:

  1 #include<sys/wait.h>
  2 #include<stdio.h>
  3 #include<stdlib.h>
  4 #include<unistd.h>
  5 int main()
  6 {
  7     pid_t id = fork();
  8     if(id == 0)
  9     {
 10         int cnt = 5;
 11         while(cnt)
 12         {
 13             printf("子进程正在执行,cnt:%d\n", cnt--);
 14             sleep(1);
 15         }
 16         exit(11);
 17     }
 18     else {
 19         printf("父进程开始执行!\n");
 20         int statu= 0;
 21         pid_t ret = wait(&statu);  // wait只能阻塞式等待                                                                                                                                                     
 22         if(ret > 0)
 23         {
 24             printf("等待成功!,pid:%d, 退出码:%d\n",ret, WEXITSTATUS(statu));
 25         }
 26     }
 27     return 0;
 28 }

waitpid

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

options:
    用于选择waitpid是以阻塞式等待还是非阻塞式等待。
    WNOHANG: = 1 表示非阻塞式等待,
    0: 表示阻塞式等待。等待子进程运行结束再返回
pid:
    -1 表示等待任意一个子进程,与wait等效(wait不能选定子进程)
    >0 表示指定等待id为pid的子进程。
status:
    输出型参数,用于获取子进程的退出情况:是否正常退出以及退出码
    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
    上面两个是宏,用于方便提取子进程执行信息。
返回值:
    基于options
    若阻塞式等待,则子进程结束返回子进程pid
    若非阻塞式等待,子进程结束返回子进程pid,子进程没结束,则因为非阻塞式等待,直接返回0.
    若函数调用失败,则返回-1

一个阻塞式等待测试代码

  1 // 这个cpp文件用于测试父进程waitpid采用阻塞式等待子进程                                                                                                                                                      
  2 #include<stdio.h>
  3 #include<stdlib.h>
  4 #include<unistd.h>
  5 #include<sys/wait.h>
  6 int main()
  7 {
  8     pid_t id = fork();
  9     if(id == 0)
 10     {
 11         // 子进程
 12         printf("子进程开始执行,pid:%d\n",getpid());
 13         int cnt = 5;
 14         while(cnt)
 15         {
 16             printf("子进程正在执行!!! cnt:%d\n", cnt--);
 17             sleep(1);
 18         }
 19         exit(66);
 20     }
 21     else {
 22         // 这里是父进程,父进程执行这里的代码
 23         printf("父进程开始执行!!\n");
 24         int status = 0;
 25         pid_t ret = waitpid(id, &status, 0);
 26         if(ret > 0)
 27         {
 28            // 表示ret为子进程的pid,表示子进程已退出
 29            printf("子进程已退出!\n");
 30            if(WIFEXITED(status))
 31            {
 32                printf("子进程成功运行结束!退出码为:%d,退出信号:%d\n", WEXITSTATUS(status),status & 0x7F);
 33            }
 34            else {
 35                printf("子进程执行错误,信号:%d,|%d\n",status & 0x7F ,WIFEXITED(status));
 36            }
 37         }
 38         else if(ret == 0)
 39         {
 40             // 只有设置为WNOHANG的时候,才可能返回0,表示子进程没有执行结束,你可以干你自己的事情
 41             printf("此处不可能出现!\n");
 42         }
 43         else 
 44         {
 45             // 返回-1,表示waitpid发生错误。
 46             printf("waitpid执行错误!\n");
 47         }
 48     }
 49 }   

非阻塞式等待测试代码:

  1 #include<iostream>                                                                                                                                                                                           
  2 #include<vector>
  3 #include<stdio.h>
  4 #include<stdlib.h>
  5 #include<sys/wait.h>
  6 #include<unistd.h>
  7 #include<algorithm>
  8 
  9 //typedef void (*test)();
 10 std::vector<void(*)()> v;
 11 
 12 std::vector<int> v_int = {5,4,6,7,8,0,1,3,2,9};
 13 
 14 void func1()
 15 {
 16 //    std::sort(v_int.begin(), v_int.end());    
 17     std::cout<<"func1 success!!!"<<std::endl;
 18 }
 19 
 20 void func2()
 21 {
 22 //    for(auto&i : v_int)
 23 //        std::cout<<i<<" ";
 24 //    printf("\n");
 25     std::cout<<"func2 success!!!"<<std::endl;
 26 }
 27 
 28 void Load_xinwupangwu()
 29 {
 30     v.push_back(func1);
 31     v.push_back(func2);
 32 }
 33 
 34 int main()
 35 {
 36     pid_t id = fork();
 37     if(id < 0)
 38     {
 39         std::cout<<"创建子进程失败!!!"<<std::endl;
 40     }
 41     else if(id == 0)
 42     {
 43         // 子进程部分
 44         int cnt = 7;
 45         while(cnt)
 46         {
 47             printf("子进程正在执行,cnt:%d\n", cnt--);
 48             sleep(1);
 49         }
 50         int* p = nullptr;
 51         *p = 10;
 52         exit(17);
 53     }
 54     else {
 55         // 父进程部分
 56         std::cout<<"父进程开始执行!!"<<std::endl;
 57         bool flag = false;
 58         int status = 1; 
 59         while(!flag)
 60         {
 61             pid_t ret = waitpid(id, &status, WNOHANG); // 表示非阻塞式等待子进程状态改变
 62             if(ret > 0)
 63             {
 64                 // 子进程退出!
 65                 if(WIFEXITED(status))
 66                 {
 67                     printf("子进程退出,且执行完毕,退出信号:%d, 退出码:%d\n", status&0x7F, WEXITSTATUS(status));
 68                 }                                                                                                                                                                                            
 69                 else {
 70                     printf("子进程退出,程序崩溃,退出信号:%d\n", status&0x7F);
 71                 }
 72                 flag = true;
 73             }
 74             else if(ret == 0)
 75             {
 76                 // 表示子进程还没退出
 77                 // 此时父进程可以做一些自己的事情
 78                 printf("子进程还没退出,父进程非阻塞式等待,可以做自己的事!\n");
 79                 if(v.empty())
 80                 {
 81                     Load_xinwupangwu();
 82                 }
 83                 for(auto i : v)
 84                 {
 85                     i();
 86                 }
 87                 sleep(1);
 88             } 
 89             else {
 90                 printf("waitpid出现错误!\n");
 91                 flag = true;
 92             }
 93         }
 94     }
 95     return 0;
 96 }        


wait/waitpid总结:

1. wait只能阻塞式等待,waitpid可以选择等待方式,依据的是最后一个参数:0 or WNOHANG。

2. status是输出型参数,由操作系统填充,用于得到子进程的执行结果:正常 or 崩溃(看信号),正常的话,是否正确(看退出码)

3. status参数类型是int*,但是,并非以整型整体来获取子进程执行结果。而是按照比特位的方式

=》 status只研究低16比特位。

 由上图可知,若正常终止,次低八位,表示退出码。 (status>>8) & 0xff
                      若崩溃,则低7位为终止信号。status & 0x7f     0x7f = 1111111

通过以上方式,即可得到status所标识的子进程退出信号 or 子进程退出码

 4. 联想僵尸进程:僵尸进程为进程退出后,退出状态没有被父进程读取。所以,僵尸进程至少保留进程的PCB中的信息。事实上,每个进程的task_struct里面保存了对应进程的退出结果信息。

 上图为PCB源码中的一个数据成员,即退出码exit_code 退出信号exit_signal。

所以,wait/waitpid作为操作系统系统接口,它是完全可以取进程PCB中的退出码和退出信号的。本质就是读取子进程的task_struct结构!

5. WNOHANG就是宏定义,#define WNOHANG 1   这种1,比较奇怪的数字,称为魔鬼数字/魔术数字。

6. 后面编写多进程的程序,基本写法就是fork + wait/waitpid


进程替换!

进程替换的概念和原理:

用fork创建子进程后,可以用进程替换的方式,通过函数调用让子进程去执行一个全新的进程。当进程调用一种exec函数时,物理内存中保存的该进程的数据和代码将全部被替换为新的进程的代码和数据。之后,子进程会将新的进程完整执行一遍!

=》 进程替换,并没有创建新的进程,而是完全替换了一个已存在进程的代码+数据为新进程的代码+数据。所以进程的id,优先级等都不会变!

=》 我们知道,fork之后父子进程的代码共享,数据写时拷贝。那么,子进程进行进程替换时,数据和代码被替换,本质就是数据和代码被写入。所以此时代码也会发生写时拷贝。再将子进程的代码和数据替换为新进程的代码和数据。以此保证父子进程的独立性!!!

=》 exec系列函数本质上,就是把新的进程加载到内存中!

exec系列,进程替换接口函数:

1.       int execl(const char *path, const char *arg, ...);
2.       int execlp(const char *file, const char *arg, ...);
3.       int execle(const char *path, const char *arg, ...,
                  char * const envp[]);
4.       int execv(const char *path, char *const argv[]);
5.       int execvp(const char *file, char *const argv[]);
6.       int execvpe(const char *file, char *const argv[],
                   char *const envp[]);
7.       int execve(const char *path, char *const argv[], 
                  char *const envp[]);

函数介绍:

l(list) : 表示arg命令行参数采用列表形式
v(vector) : 表示arg命令行参数采用数组形式
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

第一个参数,传递要替换为的进程的路径+目标文件名,相对路径或绝对路径。 而如果函数名有p表示,第一个参数还可以传递环境变量里保存好的,比如ls,pwd。若没p,则必须传递绝对/相对路径

第二个参数,为命令行参数,在命令行上怎么执行这个进程,这里就怎么填。以列表形式 or 数组形式

第三个参数:若函数名中有e,则表示自己维护环境变量(有关这里环境变量的作用,不太理解,只能勉强用一用)


1. exec系列函数若调用失败返回-1,若调用成功没有返回值。(也无法返回)

2. execve是真正的系统调用函数,上面的六个函数都是对execve的封装,适用于不同的使用场景。

3. 上方介绍的使用是最官方,最严谨使用,实际上exec系列函数可能会有一些缺省行为,也就是某个参数,你不传,它根据你传的信息也能自动补全。当然,初期学习我们还是要全面一点。

4. 本质上,第一个参数,传路径是为了让exec找到这个程序在哪里。后面args为命令行参数,意思是怎么执行这个参数。而命令行参数,比如以NULL结尾。

测试代码:

    1 #include<stdio.h>
    2 #include<unistd.h>
    3 #include<stdlib.h>
    4 #include<sys/wait.h>
    5 
W>  6 int main(int argc, char* argv[], char* env[])
    7 {
    8     pid_t id = fork();
    9     if(id == 0)
   10     {
   11         printf("子进程开始执行\n");
   12         sleep(3);
   13         //execl("/usr/bin/ls","ls",NULL);  // 这里必须加后面的ls
   14         //execl("/usr/bin/pwd","pwd",NULL); // 这里必须加后面的"pwd" ,说明,这种都需要!
   15         //execl("./../proc","./proc",NULL);
   16         //execlp("ls","ls","-l","-a",NULL);
   17 
   18         char* _arg[] = {(char*)"ls",(char*)"-a",(char*)"-l",NULL};
   19         //execvp("ls",_arg);
   20         const char* myfile = "/usr/bin/ls";
   21         //execv(myfile,_arg);
   22 
   23         execve(myfile, _arg, env);
   24 
   25         exit(3);
   26     }
   27     else {
   28         // 父进程
   29         printf("父进程开始执行\n");                                                                                                                                                                        
   30         int status = 0;
   31         pid_t ret = waitpid(id, &status, 0);
   32         if(ret > 0) // 上方为阻塞式等待,返回id或者-1
   33         {
   34             printf("等待成功,退出码为:%d\n", WEXITSTATUS(status));
   35         }
   36         else {
   37             printf("waitpid函数调用失败\n");
   38         }
   39     }
   40     return 0;
   41 }                                                                                                                                                                                                          
 

 综合进程创建,终止,等待,转换写的一个简易shell:

    1 #include<stdio.h>                                                                                                                                                                                          
    2 #include<unistd.h>
    3 #include<stdlib.h>
    4 #include<sys/wait.h>
    5 #include<string.h>
    6 
    7 
    8 #define NUM 512
    9 #define SIZE 32
   10 
   11 char Mess[NUM];
   12 char* g_argv[SIZE];
   13 int main()
   14 {
   15     while(1)
   16     {
   17         // 打印提示符
   18         printf("[yzl@VM-4-5-centos myshell]# ");
   19         fflush(stdout);
   20         memset(Mess, '\0', sizeof Mess);
   21         
   22         // 读取命令
   23         if(fgets(Mess, sizeof Mess, stdin) == NULL)  // 此处会把\n也读取进去!
   24             continue;
   25         Mess[strlen(Mess)-1] = '\0'; 
   26         //printf("%s",Mess);
   27         
   28         // 提取命令
   29         g_argv[0] = strtok(Mess, " ");
   30         int index = 1;
   31         if(strcmp(g_argv[0],"ls") == 0)
   32         {
   33             g_argv[index++] = (char*)"--color=auto";
   34         }
   35         if(strcmp(g_argv[0], "ll") == 0)
   36         {
W> 37             g_argv[0] = "ls";
W> 38             g_argv[index++] = "-l";
W> 39             g_argv[index++] = "--color=auto";
   40         }
W> 41         while(g_argv[index++] = strtok(NULL, " ")) 
   42             ;
   43         
   44         if(strcmp(g_argv[0], "cd") == 0)
   45         {
   46             if(g_argv[1] != NULL)
   47                 chdir(g_argv[1]);
   48             continue;
   49         }
   50 
   51 
   52         pid_t id = fork();
   53         if(id == 0)
   54         {
   55             printf("下面功能让子进程进行\n");
   56             execvp(g_argv[0], g_argv);
   57             exit(-1);
   58         }
   59         int status = 0;
   60         pid_t ret = waitpid(id, &status, 0); //阻塞式等待
   61         if(ret > 0)
   62         {
   63             printf("exit code:%d\n", WEXITSTATUS(status));
   64         }
   65     }
   66     return 0;                                                                                                                                                                                              
   67 }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值