自学内容网 自学内容网

Linux:进程(三)

1. 进程创建补充

fork之后父子两个执行流分别执行,fork之后谁谁先执行由调度器来决定。

一般,父子代码共享。当父子不再写入时,数据也是共享的,但是当有一方要写入,就触发写时拷贝

fork调用失败的原因

1. 系统中有太多进程

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

2. 进程终止

进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的代码和数据。

 进程退出主要有以下三个场景

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

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

3. 代码异常终止

 如果是1和2这种正常终止的情况可以通过 echo $ ?  来查看进程退出码

退出码

退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束后,我们可以知道命令是正确完成了还是错误结束的,是操作系统用来判断进程是否完成任务的方式。程序返回退出代码0时表示执行成功,返回0以外的任何代码都被视为不成功

正常退出有以下方法

1. 从main返回 

2. 调用exit   头文件<stdlib.h>

3. 调用_exit  头文件<unistd.h>

#include<stdio.h>
 #include<stdlib.h>
 #include<unistd.h>
 int main()
 {
     printf("hello world\n");

     //_exit(101);                                                                         
    exit(100);
     return 33;
 }     

 以下结果分别是return  exit 与 _exit的结果

 

当代码异常终止时:退出码无意义

程序的进程退出码是写在task_struct内部的,我们可以通过strerror函数来获取退出码的描述,

strerror(1);
strerror(2);
....
exit与_exit函数
#include<unistd.h>
void _exit(int status);
//status定义了进程的终止状态,父进程通过wait来获取该值
//status是int,但是只有低八位(后面说明)可以被父进程使用_exit(-1),echo $? 到的返回值是255


#include<stdlib.h>
void exit(int status);

 exit最后也会调用_exit,但是还要先执行一些操作

1. 执行用户通过atexit或者on_exit定义的清理函数。

2. 关闭所有打开的流,所有缓存数据都被写入

3. 调用_exit

 _exit就直接退出进程,不会对缓冲区进行刷新

用以下代码来验证一下

#include<stdio.h>
 #include<stdlib.h>
 #include<unistd.h>
 int main()
 {
     printf("hello world");
    
    //先测试完_exit再将其删掉测试exit
    _exit(101);                                                                         
    exit(100);
     return 0;
 }     

 _exit结果如下

exit结果如下

 所以缓冲区一定不是操作系统内部的,在用户空间中,C语言标准库提供

三种方式

exit 是C库里的

_exit 是系统给的

但是实际上exit内部(底层封装)是用了_exit

main函数中的return表示进程完成而其他函数中的return只表示自己的函数调用完成,但是exit与_exit在哪调用都表示进程结束并返回给父进程bash子进程的退出码。

main函数中的 return x; 等价于执行exit(x); 因为在main函数运行结束后会将main函数的返回值当做exit参数来调用exit函数

 3. 进程等待

为什么要有进程等待

1.子进程退出,父进程如果不管不顾,就可能造成僵尸进程内存泄漏

2.变成僵尸进程后,就连kill -9 也对它没有办法,因为不能杀死一个死去的进程

3.我们需要知道父进程给子进程的任务如何了,例如子进程运行完成结果是否正确,是否正常退出了。

4.父进程通过等待的方式来回收子进程资源,获取子进程退出信息(可以选择的)

 wait函数与waipid函数
//头文件
#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int* status);

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

wait函数:

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

参数:输出型参数,用来获取子进程退出状态,不关心可以设置成NULL。

waitpid函数:

返回值:

正常返回时waitpid返回收集到的子进程的进程id

如果第三个参数选择了WNOHANG,而调用waitpid发现没有已退出的子进程可收集,则返回0

如果调用中出错,则返回-1,这时errno会设置成相应的值以指示错误所在;

参数

pid:pid==-1,等待任一个子进程,与wait等效。pid>0,等待其进程ID与pid相等的子进程。

status:输出型参数,不关心可设置为NULL

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

设置为WNOHANG时:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待,若正常结束返回该子进程的ID

status参数

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

如果传递NULL,表示不关心子进程退出状态信息。否则,操作系统会根据该参数,将子进程退出信息反馈给父进程。

status不能简单的当成整形来看,可以当做位图来看,如下图所示

 低七个比特位全0,一旦不是全0就是异常退出的,退出码就无意义了

 status在代码正常运行完毕的情况下是进程退出码,异常终止是保存异常时所对应的信号编号。

所以正常看高8位,异常时看低7位,第8位是coredump 标志用一下操作

(status>>8)&0xff;//获取高八位的值

status&0x7f;//获取第七位的值

这两个操作,操作系统为我们提供了两个宏来表示对应的退出码和退出信号

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

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

#include<stdio.h>
 #include<stdlib.h>
 #include<unistd.h>
 #include<sys/wait.h>
 #include<errno.h>
 #include<string.h>
 #include<sys/types.h>
 int main()
 {
     pid_t pid;
     if((pid=fork())==-1)
     {
         perror("fork");
         exit(1);
     }
     if(pid==0)
     {
         printf("子进程pid:%d\n",getpid());
         sleep(20);
         exit(10);
     }
      else
     {
         int st;
         int ret=wait(&st);//
 
         if(ret>0&&(st&0x7F)==0)//后七位为0
         {                                                                                 
             printf("子进程退出代码为:%d\n",(st>>8)&0xFF);
        }
         else if(ret>0)
             printf("终止信号:%d",st&0x7F);
     }
    return 0;
}

 让子进程正常终止

父进程成功获取退出信息。运行中在另一台终端将子进程kill掉

输出结果是9,父进程同样等待成功了

我们来使用waitpid与两个宏来看一下代码如下

 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/wait.h>
  5 #include<errno.h>
  6 #include<string.h>
  7 #include<sys/types.h>
  8 int main()
  9 {
 10     pid_t pid;
 11     if((pid=fork())==-1)
 12     {
 13         perror("fork");
 14         exit(1);
 15     }
 16     if(pid==0)
 17     {
 18         printf("子进程pid:%d\n",getpid());
 19         sleep(20);
 20         exit(10);
 21     }
        else if(pid>0)
 23     {
 24         int st;
 25         int ret=waitpid(pid,&st,0);//
 26                                                                                           
 27         if(ret>0&&WIFEXITED(st))//子进程正常结束返回真
 28         {
 29             printf("子进程退出代码为:%d\n",WEXITSTATUS(st));
 30         }
 31         else if(ret>0)
 32             printf("终止信号:%d",st&0x7F);
 33     }
 34     return 0;
 35 }

结果一样 

阻塞等待

验证阻塞等待

#include<stdio.h>
 #include<stdlib.h>
 #include<unistd.h>
 #include<sys/wait.h>
 #include<errno.h>
 #include<string.h>
 #include<sys/types.h>
 int main()
 {
     for(int i=0;i<10;i++)
     {
        pid_t pid;
         pid=fork();
        if(pid<0)
         {
             printf("%s fork error\n",__FUNCTION__);//一个宏会自动替换为该函数名字
        }
         else if(pid==0)
         {
             pid_t p1=getpid();
             printf("此子进程pid:%d\n",p1);
             sleep(2);
            exit(11);
         }
        else if(pid>0)
         {
             int status=0;
            pid_t ret=waitpid(-1,&status,0);//阻塞式等待                                  
             printf("检测waitpid \n");
            if(ret==pid&&WIFEXITED(status))
             {
                printf("等待孩子成功,return pid: %d\n",WEXITSTATUS(status));
            }
            else
             {
                printf("失败\n");
                 return 1;
             }
         }
 
    }
    return 0;
}

非阻塞等待

大部分的父子进程关系中,当子进程未退出时,父进程通常处于阻塞等待的状态,在此期间父进程不能进行其他操作,我们可以通过将waitpid的第三个参数设置为WNOHANG,即在没有子进程退出时立即返回不等待,等待方可以做自己的事情(通过函数指针或者function<>;包装器)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
 #include <sys/wait.h>

 void test()
 {
    printf("hello\n");
}
int main()
{
            pid_t pid;
         pid=fork();
         if(pid<0)
          {
             printf("%s fork error\n",__FUNCTION__);//一个宏会自动替换为该函数名字
        }
          else if(pid==0)
         {
             pid_t p1=getpid();
              printf("此子进程pid:%d\n",p1);
              sleep(2);
            exit(11);
         }
            else
         {
              int status=0;
            pid_t ret=waitpid(pid,&status,WNOHANG);                                                               
            //pid_t ret=waitpid(pid,&status,0);//阻塞式等待
            test();
            test();
             test();
             printf("检测waitpid \n");
             if(ret==pid&&WIFEXITED(status))
             {
               printf("等待孩子成功,return pid: %d\n",WEXITSTATUS(status));
             }
            else
              {
                 printf("失败\n");
                 return 1;
              }
         }
        return 0;
}

阻塞等待先等子进程完毕再进行自己的操作,子进程不结束,父进程会阻塞在wait/waitpid调用处

非阻塞等待,先执行了函数调用,waitpid发现没有要退出的子进程直接返回0

另外:

如果子进程已经退出了,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。

如果不存在该子进程,则立即出错返回

 4. 进程替换

我们通过fork函数创建子进程后,父子会各自执行父进程的一部分代码,我们可以通过进程替换来让子进程执行一个全新的程序。

进程程序替换的概念

程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间之中。

用fork创建子进程后,执行的是和父进程相同的程序(但有可能通过if else执行不同的代码分支),子进程一般通过调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新进程替换,从新程序的启动例程开始执行。但是调用exec不会创建新的进程所以进程替换前后这个进程的id是不会变化的

其原理如下图所示

进程替换函数

以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[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

需要注意

一旦程序替换成功了,就去执行新的代码了,原始代码的后半部分就不存在了

所以exec函数只有失败返回值,没有成功返回值

调用失败返回-1

在进程替换的过程中不是创建了新的进程只是把当前进程的代码和数据覆盖式的进行替换

不会影响父进程(进程具有独立性)->会释放旧空间(子进程首先释放其当前占用的地址空间,包括之前从父进程继承并共享的代码段、数据段、堆和栈等)->重新分配空间(根据新程序要求为子进程重新分配新的地址空间,这个地址空间是专门为新程序准备的)->加载新程序(通过加载器使得程序从新程序的入口点开始执行)

execl函数

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

path:路径+程序名(要执行谁)

arg:可变参数列表(怎么执行它)

结尾必须以NULL表明参数传递完成

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>

int main()
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork() error\n");
    }
    if(id==0)
    {
        printf("child\n");
        execl("/usr/bin/ls","ls","-l","-a",NULL);

        perror("execl error\n");
        exit(-1);
    }
    else if(id>0)
    {
        int status;

        pid_t tmp=waitpid(id,&status,0);
        if(tmp<0)
        {                                                                                                                                                                                                          
            perror("waitpid error\n");
            exit(1);
        }
        if(WIFEXITED(status))
        {
            printf("exit code:%d\n",WEXITSTATUS(status));
        }
    }

    return 0;
}

将上面的语句替换为下方语句来执行一个cpp程序

        execl("./hello","./hello",NULL);

 即在当前目录下寻找hello,并执行

当然也可能调用python或者是java的程序

execlp函数

int execlp(const char *file, const char *arg, ...);
函数名字比上面多了一个p,这个p表示环境变量,无需写全路径了

file只要说明要执行的文件名即可(execlp会自动在环境变量PATH中查找指定的命令)作用:要执行谁

arg:同上可变模板参数(要怎么执行)

NULL结尾

execlp("ls", "ls", "-a", "-l", NULL);//执行ls -a -l
execlp("./hello", "./hello", NULL);//执行C++程序

execv函数

int execv(const char *path, char *const argv[]);

名字比第一个多了v:即vector数组

path:路径+程序名(要执行谁)

argv:一个命令行参数表(一个指针数组)

char* myargv[] = { "ls", "-a", "-i", "-l", NULL };//通过指针数组
execv("/usr/bin/ls", myargv);

execle函数

 int execle(const char *path, const char *arg, ...,char *const envp[]);

path:路径+程序名

arg:可变参数列表

envp:自己设置的环境变量(传到替换程序的main参数env数组中)

char* env[] = { (char *cibst)"MYVAL=2025", NULL };//自己的环境变量
execle("./hello", "./hello", NULL, env);//执行./hello

其他的参数就是将以上的组合起来使用

 总结一下

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

v(vector):参数用数组

p(path):有p自动搜索环境变量PATH

e(env):表示自己维护环境变量

函数名参数格式是否使用当前环境变量
execl列表
execlp列表
execle列表需要自己组装环境变量
execv数组
execvp数组
execve数组需要自己组装环境变量

只有execve是真正的系统调用其他的最终都调用了execve,所以execve在man手册的第二节,其他函数在man手册第三节。各个函数关系如下图所示

 


这篇就到这里啦(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤


原文地址:https://blog.csdn.net/m0_68142120/article/details/145147325

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!