【Linux】进程二 (PCB fork/vfork wait/waitpid exit/_exit exec函数族 环境变量)

Gretel ·
更新时间:2024-11-13
· 808 次阅读

一、描述进程——PCB ·进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合 ·我们称为PCB,Linux操作系统下的PCB是:task struct 2、task_struct——PCB的一种

·在Linux中描述进程的结构体叫做task_struct.

·task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里包含着进程的信息

3.task_struct内容分类

·标示符:描述本进程的唯一标示符,用来区别其他进程

·状态:任务状态,退出代码,退出信号

·优先级:相对于其他进程的优先级

·程序计数器:程序中即将被执行的下一条指令的地址

·内存指针:包括程序代码和进程相关的指针,还有和其他进程共享的指针

·上下文数据:进程执行时处理器的寄存器中的数据

·I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表

·记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等

给张图方便理解:

二、进程创建(fork&vfork)

在Linux系统内,创建子进程的方法是使用系统调用fork()函数。fork()函数是Linux系统内一个非常重要的函数,它与我们之前学过的函数有一个显著的区别:fork()函数调用一次却会得到两个返回值。

fork()函数

 所需头文件:#include

                      #include

 函数原型:   pid_t fork()

 函数返回值:

                       == 0     子进程

                        >0        父进程,返回值为创建出的子进程的PID

                        <0        出错

 fork()函数用于从一个已经存在的进程内创建一个新的进程,新的进程称为“子进程”,相应地称创建子进程的进程为“父进程”。使用fork()函数得到的子进程是父进程的复制品,子进程完全复制了父进程的资源,包括进程上下文、代码区、数据区、堆区、栈区、内存信息、打开文件的文件描述符、信号处理函数、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等信息,而子进程与父进程的区别有进程号、资源使用情况和计时器等。

练习:创建一个子进程,打印父子进程的pid #include #include #include int main() { printf("linux66\n"); pid_t ret = fork(); if(ret < 0) { perror("失败"); return 0; } else if(ret == 0 ) { //child printf("i am child :[%d] - [%d] \n", getpid(),getppid()); } else { printf("i am father :[%d] - [%d] \n", getpid(),getppid()); } return 0 ; }

在使用fork()函数创建子进程的时候,要有一个概念:在调用fork()函数前是一个进程在执行这段代码,而调用fork()函数后就变成了两个进程在执行这段代码。两个进程所执行的代码完全相同,都会执行接下来的if-else判断语句块。

当子进程从父进程内复制后,父进程与子进程内都有一个"pid"变量:在父进程中,fork()函数会将子进程的PID返回给父进程,即父进程的pid变量内存储的是一个大于0的整数;而在子进程中,fork()函数会返回0,即子进程的pid变量内存储的是0;如果创建进程出现错误,则会返回-1,不会创建子进程。

 fork()函数一般不会返回错误,若fork()函数返回错误,则可能是当前系统内进程已经达到上限,或者内存不足。

注意:父子进程的运行先后顺序是完全随机的(抢占式执行),也就是说在使用fork()函数的默认情况下,无法控制父进程在子进程前进行还是子进程在父进程前进行。

vfork()函数

     所需头文件:#include

                         #include

     函数原型:pid_t vfork()

     返回值:

                        == 0     子进程

                        >0        父进程,返回值为创建出的子进程的PID

                        <0        出错

    vfork()函数功能与fork()函数功能类似不过更加彻底:内核不再给子进程创建虚拟空间,直接让子进程共享父进程的虚拟空间。当父子进程中有更改相应段的行为发生时,再为子进程相应的段创建虚拟空间并分配物理空间。在vfork()函数创建子进程后父进程会阻塞,保证子进程先行运行。

vfork()函数创建的子进程会与父进程(在调用exec函数族函数或exit()函数前)共用地址空间,此时子进程如果使用变量则会直接修改父进程的变量值。因此,vfork()函数创建的子进程可能会对父进程产生干扰。另外,如果子进程未调用exec函数族函数或exit()函数,则父子进程会出现死锁现象。

fork()函数与vfork()函数的主要区别如下:

    1.vfork()函数保证子进程先行运行,在子进程调度exec函数族函数或者exit()函数后父进程才会被调度运行。如果子进程需要依赖父进程的进一步动作,则会产生死锁

    2.fork()函数需要拷贝父进程的进程环境,而vfork()函数则不需要完全拷贝父进程的进程环境,在子进程调用exec函数族函数或者exit()函数之前,子进程与父进程共享进程环境(此时子进程相当于线程),父进程阻塞等待。

三、进程状态&环境变量   1、进程状态: R:运行状态 S:睡眠状态 D:磁盘睡眠状态 T:暂停状态 Z:僵尸状态(僵尸进程)
孤儿进程:

孤儿进程不是进程状态,而是一种进程的种类名称

孤儿进程产生原因:是由于父进程先于子进程退出,导致子进程需要将自己的退出状态信息返回给 f进程。

这个进程通常是操作系统当中的1号进程,也称之为Init程。Init程本身就会创建许多进程,本身Init程创建的子进程我们不能称他们为孤儿进程

2、环境变量

·环境变量是为了让我们的当前的操作系统执行的更加愉快

·常见的环境变量:

 PATH:查看可执行程序的环境变量

 HOME :保存是当前用户的家目录的环境变量

 SHELL :保存当前所使用的shell的环境变量

 LD_LIBRARY_PATH:程序运行时,库文件的搜索路径的环境变量

 LIBRARY_PATH :程序编译的时候,库文件的搜索路径的环境变量

·常见的命令:

 echo $[环境变量名称]:查看特定的环境变零值

export(在命令行中修改,只在当前bash中生效)

             export[环境变量名称]=$[环境变量名称]:[新增的环境变量]

3.环境变量的组织方式

4.使用代码获取环境变量 1.从main函数的参数中获取 #include int main(int argc, char* argv[], char* env[]) { int i = 0; for(; i < argc; i++) { printf("argv[%d] = [%s]\n", i, argv[i]); } for(i = 0; env[i]; i++) { printf("env[%d] = [%s]\n", i, env[i]); } return 0; }

argc:命令行参数的个数

argv[0]:命令行参数

env[0]:当前用户的环境变量

2.从environ中获取 #include #include int main() { extern char** environ; for(int i = 0; environ[i]; i++) { printf("environ[%d] = [%s]\n", i, environ[i]); } return 0; } 3.使用getenv函数获取环境变量 #include #include int main() { char* path = getenv("PATH"); printf("path = [%s]\n", path); return 0; } 四、进程虚拟地址空间

写时拷贝:当父进程定义了一个全局变量,fork出来一个子进程,当自己成发生对该全局变量发生修改的时候,操作系统会重新开辟一段空间来保存子进程更改后的值,即子进程会更改自己的页表映射关系 五、进程终止(exit&_exit)

当我们需要结束一个进程的时候,我们可以使用exit()函数或_exit()函数来终止该进程。当程序运行到exit()函数或_exit()函数时,进程会无条件停止剩下的所有操作,并进行清理工作,最终将进程停止。

exit()函数

所需头文件:#include

函数原型:    void exit(int status)

函数参数:status表示让进程结束时的状态(会由主进程的wait();负责接收这个返回值【也可以不接收】-->类似函数的返回值),默认使用0表示正常结

 _exit()函数

所需头文件:#include

函数原型:void _exit(int status)

函数参数: status表示让进程结束时的状态(会由主进程的wait();负责接收这个返回值【也可以不接收】-->类似函数的返回值),默认使用0表示正常结 

   

exit()函数与_exit()函数用法的区别的:

    _exit()函数直接使进程停止运行,当调用_exit()函数时,内核会清除该进程的内存空间,并清除其在内核中的各种数据。

    exit()函数则在_exit()函数的基础上进行了升级,在退出进程之间增加了若干工序。exit()函数在终止进程之前会检测进程打开了哪些文件,并将缓冲区内容写回文件。

    exit()函数与_exit()函数最主要的区别就在于是否会将缓冲区数据保留并写回。_exit()函数不会保留缓冲区数据,直接将缓冲区数据丢弃,直接终止进程运行;而exit()函数会将缓冲区内数据写回,待缓冲区清空后再终止进程运行。

练习:exit与_exit  #include #include //使用exit()函数终止进程 int main() {     printf("This is the content in buffer");     exit(0); } //使用_exit()函数终止进程 #include #include int main() {     printf("This is the content in buffer");     _exit(0); } 练习1会输出"This is the content in buffer",而练习2不会输出 六、进程等待(wait&waitpid) 目的:进程等待就是为了防止僵尸进程的产生 使用wait()函数与waitpid()函数让父进程回收子进程的系统资源,两个函数的功能大致类似,waitpid()函数的功能要比wait()函数的功能更多。    wait()函数

    所需头文件:#include

    函数原型: pid_t wait(int *status)

    函数参数: status    保存子进程结束时的状态(由exit();返回的值)。使用地址传递,父进程获得该变量。若无需获得状态,则参数设置为NULL

    返回值: 成功:已回收的子进程的PID

                   失败:-1

        

waitpid()函数

    所需头文件:  #include

    函数原型:      pid_t waitpid(pid_t pid, int *status, int options)

    函数参数:

          1.  pid           pid是一个整数,具体的数值含义为:

            pid>0       回收PID等于参数pid的子进程

            pid==-1    回收任何一个子进程。此时同wait()

            pid==0     回收其组ID等于调用进程的组ID的任一子进程

            pid<-1      回收其组ID等于pid的绝对值的任一子进程

         2. status     保存子进程结束时的状态(由exit();返回的值)。使用地址传递,父进程获得该变量。若无需获得状态,则参数设置为NULL

         3.  options    

            0:同wait(),此时父进程会阻塞等待子进程退出

            WNOHANG:若指定的进程未结束,则立即返回0(不会等待子进程结束)

    返回值:

          >0      已经结束运行的子进程号

          0        使用WNOHANG选项且子进程未退出

         -1        错误

wait与waitpid的区别:

    当进程结束时,该进程会向它的父进程报告。wait()函数用于使父进程阻塞,直到父进程接收到一个它的子进程已经结束的信号为止。如果该进程没有子进程或所有子进程都已结束,则wait()函数会立即返回-1。

    waitpid()函数的功能与wait()函数一样,不过waitpid()函数有若干选项,所以功能也比wait()函数更加强大。实际上,wait()函数只是waitpid()函数的一个特例而已,Linux内核总是调用waitpid()函数完成相应的功能。
 

阻塞与非阻塞:

阻塞:进程或线程执行到这函数时必须等待某个事件发生,如果事件没有发生,进程或线程就被阻塞,函数不能立刻返回

非阻塞:进程或线程执行函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况。如果事情发生则与阻塞的方式相同,若事情没有发生,则返回一个值,告知事情未发生,而进程或线程继续执行,所以效率较高

练习1:使用wait()函数,让父进程在子进程结束后再运行  #include #include #include int main() {    pid_t pid = fork();     if(pid==-1)     {         perror("can not fork");         return -1;     }     else if(pid==0)//子进程     {         printf("This is Child process\n");         printf("Child process ID is %d\n",getpid());         printf("Child process will exit\n");     }     else//父进程     {         pid = wait(NULL);//等待子进程结束         printf("This is Parent process\n");         printf("Child process %d is over\n",pid);     }     return 0; }  练习2:使用waitpid()函数,让父进程回收子进程。参数使用WNOHANG使父进程不会阻塞,若子进程暂时未退出,则父进程在1s后再次尝试回收子进程 #include #include #include #include int main() {     pid_t pid;     pid = fork();     if(pid<0)     {         perror("cannot fork");         return -1;     }     else if(pid==0)//子进程     {         printf("This is Child process\n");         sleep(5);//模拟子进程运行5s         exit(0);//子进程正常退出     }     else//父进程     {         int ret;         do//循环直至子进程退出为止         {             ret = waitpid(pid,NULL,WNOHANG);//回收子进程,使用WNOHANG选项参数             if(ret==0)             {                 printf("The Child process is running, can't be exited\n");                 sleep(1);//1秒后再次尝试             }         }while(ret==0);         if(pid==ret)//如果检测到子进程退出         {             printf("Child process exited\n");         }         else         {             printf("Some error occured\n");         }     }     return 0; } 七、进程程序替换(exec函数族) 所需头文件:#include

    函数原型:

        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[])

        execl(完整的路径名,列表……);

        execlp(文件名,列表……);

        execle(完整的路径,列表……,环境变量的向量表)

        execv(完整的路径名,向量表);

        execvp(文件名,向量表);

        execve(完整的路径,向量表,环境变量的向量表)    

    函数参数:

        path:文件路径,使用该参数需要提供完整的文件路径

        file:文件名,使用该参数无需提供完整的文件路径,终端会自动根据$PATH的值查找文件路径

        arg:以逐个列举方式传递参数

        argv:以指针数组方式传递参数

        envp:环境变量数组

    返回值: -1(通常情况下无返回值,当函数调用出错才有返回值-1)

    这6个函数的函数功能类似,但是在使用语法规则上有细微区别。我们可以看出,其实exec函数族的函数都是exec+后缀来命名的,具体的区别如下:

区别1:参数传递方式(函数名含有l还是v)

exec函数族的函数传参方式有两种:逐个列举或指针数组。

    若函数名内含有字母'l'(表示单词list),则表示该函数是以逐个列举的方式传参,每个成员使用逗号分隔,其类型为const char *arg,成员参数列表使用NULL结尾

    若函数名内含有字母'v'(表示单词vector),则表示该函数是以指针数组的方式传参,其类型为char *const argv[],命令参数列表使用NULL结尾

区别2:查找可执行文件方式(函数名是否有p)

我们可以看到这几个函数的形参有些为path,而有些为file。其中:

    若函数名内没有字母'p',则形参为path,表示我们在调用该函数时需要提供可执行程序的完整路径信息

    若函数名内含有字母'p',则形参为file,表示我们在调用该函数时只需给出文件名,系统会自动按照环境变量$PATH的内容来寻找可执行程序

区别3:是否指定环境变量(函数名是否有e)

exec可以使用默认的环境变量,也可以给函数传入具体的环境变量。其中:

    若函数名内没有字母'e',则使用系统当前环境变量

    若函数名内含有字母'e'(表示单词environment),则可以通过形参envp[]传入当前进程使用的环境变量

exec函数族简单命名规则如下:

    l        接收以逗号为分隔的参数列表,列表以NULL作为结束标志

    v        接收一个以NULL结尾的字符串数组的指针

    p        提供文件的完整的路径信息 或 通过$PATH查找文件

    e        使用系统当前环境变量 或 通过envp[]传递新的环境变量

这6个exec函数族的函数,execve()函数属于系统调用函数,其余5个函数属于库函数。

练习1:使用execl()函数 #include #include #include int main() {     pid_t pid = fork();     if(pid < 0 )     {         perror("cannot fork");         return 0;     }     else if(pid==0)//子进程     {         printf("i am child \n");        if(execl("/bin/ps","ps","-ef",NULL)<0)//子进程执行ps -ef,注意参数的写法,且需要使用NULL结尾 { perror("cannot exec ps"); } }     }     else//父进程     {         sleep(1);//父进程延时1s,让子进程先运行 printf("i am father \n");     }     return 0; }

在调用exec函数族的函数时,一定要加上错误判断语句。当exec函数族函数执行失败时,返回值为-1,并且报告给内核错误码,我们可以通过perror将这个错误码的对应错误信息输出。常见的exec函数族函数执行失败的原因有:

    1.找不到文件或路径

    2.参数列表arg、数组argv和环境变量数组列表envp未使用NULL指定结尾

    3.该文件没有可执行权限

练习2:使用execlp() #include #include #include int main() {     pid_t pid;     pid = fork();     if(pid==-1)     {         perror("cannot fork");         return -1;     }     else if(pid==0)//子进程     {         printf("This is Child process\n");         if(execlp("ps","ps","-ef",NULL)<0)//第一个参数只需要写ps即可,系统会根据环境变量自行寻找ps程序的位置         {             perror("cannot exec ps");         }     }     else//父进程     {         printf("This is Parent process\n");         sleep(1);//父进程延时1s,让子进程先运行     }     return 0; } 练习3:使用execvp()函数 #include #include #include int main() {     pid_t pid;     char *arg[]={"ps","-ef",NULL};//设定参数向量表,注意使用NULL结尾     pid = fork();     if(pid==-1)     {         perror("cannot fork");         return -1;     }     else if(pid==0)//子进程     {         printf("This is Child process\n");         if(execvp("ps",arg)<0)//注意该函数的参数与execlp()函数的区别         {             perror("cannot exec ps");         }     }     else//父进程     {         printf("This is Parent process\n");         sleep(1);//父进程延时1s,让子进程先运行     }     return 0; } 练习4:使用execle()函数将一个新的环境变量添加到子进程中,并使用env命令查看 #include #include #include int main() {     pid_t pid;     char *envp[]={"PATH=/tmp","USER=liyuge",NULL};//设定新的环境变量,注意使用NULL结尾     pid = fork();     if(pid==-1)     {         perror("cannot fork");         return -1;     }     else if(pid==0)//子进程     {         printf("This is Child process\n");         if(execle("/usr/bin/env","env",NULL,envp)<0)         {             perror("cannot exec env");         }     }     else//父进程     {         printf("This is Parent process\n");         sleep(1);//父进程延时1s,让子进程先运行     }     return 0; }

 练习5:使用execve()函数

#include #include #include int main() {     pid_t pid;     char *arg[]={"env",NULL};//设定参数向量表,注意使用NULL结尾     char *envp[]={"PATH=/tmp","USER=liyuge",NULL};//设定新的环境变量,注意使用NULL结尾     pid = fork();     if(pid==-1)     {         perror("cannot fork");         return -1;     }     else if(pid==0)//子进程     {         printf("This is Child process\n");         if(execve("/usr/bin/env",arg,envp)<0)         {             perror("cannot exec env");         }     }     else//父进程     {         printf("This is Parent process\n");         sleep(1);//父进程延时1s,让子进程先运行     }     return 0; } 练习6:使用execl函数族函数,在子进程内执行自己编译的可执行程序a.out文件 #include #include #include int main() {     pid_t pid;     pid = fork();     if(pid==-1)     {         perror("cannot fork");         return -1;     }     else if(pid==0)//子进程     {         printf("This is Child process\n");         if(execl("/home/linux/a.out","./a.out",NULL)<0)//使用execl()函数         {             perror("cannot exec a.out");         }     }     else//父进程     {         printf("This is Parent process\n");         sleep(1);//父进程延时1s,让子进程先运行     }     return 0; }
作者:海的早晨



waitpid exec函数 环境 pcb Linux exec wait exit 进程 环境变量 fork 变量

需要 登录 后方可回复, 如果你还没有账号请 注册新账号