linux:进程空间的回收,exec族

父子进程的关系

  • 子进程是父进程的副本:这个说法在概念上是正确的,但实际上,子进程并不是父进程的完全物理副本。在Unix和类Unix系统中,fork() 系统调用创建了一个与父进程几乎完全相同的子进程,包括环境变量、打开的文件描述符、当前工作目录等。但是,子进程和父进程在内存中的物理表示是不同的,它们各自拥有自己的地址空间。

  • 共享与独立:子进程和父进程的正文段(text segment,即程序代码)在物理上是共享的(通过操作系统的内存管理机制,如页表),但它们是只读的。数据段(data segment)、堆(heap)和栈(stack)在逻辑上是独立的,每个进程都有自己的副本。这意味着,如果你在一个进程中修改了堆或栈上的数据,这种修改不会影响到另一个进程。

(1)fork的返回值 (2)pid不同

进程的终止:8中情况

    1)main 中return
    2)exit(), c库函数,会执行io库的清理工作,关闭所有 的流,以及所有打开的文件。已经清理函数(atexit)。
    3)_exit,_Exit 会关闭所有的已经打开的文件,不执行清理函数。
    4)    主线程退出
    5)主线程调用pthread_exit
    异常终止
    6)abort()
    7)signal   kill pid
    8)最后一个线程被pthread_cancle

僵尸进程(Zombie Process)

僵尸进程是指那些已经执行完毕但尚未被其父进程通过wait()waitpid()系统调用回收其资源(如进程描述符、内核栈等)的进程。这些进程在进程表中仍保留一个条目,以便父进程能够查询其退出状态,但它们不再占用CPU或内存资源(除了进程表中的一个条目)。僵尸进程的存在本身并不是问题,但如果一个父进程创建了大量子进程而不回收它们,这些僵尸进程就会积累在进程表中,消耗系统资源。

孤儿进程(Orphan Process)

孤儿进程是指其父进程已经终止或被杀死的进程。当一个父进程终止时,它的所有子进程都将由init进程(进程ID为1)接管。init进程会定期检查其下的子进程,并使用wait()waitpid()回收它们的状态和资源,从而避免产生僵尸进程。

exit     库函数

    退出状态,终止的进程会通知父进程,自己使如何终止的。如果是正常结束(终止),则由exit传入的参数。如果是异常终止,则有内核通知异常终止原因的状态。任何情况下,负进程都能使用wait,waitpid获得这个状态,以及资源的回收。
    void exit(int status) 
    exit(1);
    功能:
        让进程退出,并刷新缓存区
    参数:
        status:进程退出的状态
    返回值:
        缺省

return和exit

  • return出现在main函数中时,它确实会结束进程的执行,并返回给操作系统一个退出状态。这个退出状态可以被父进程通过wait()waitpid()系统调用捕获。
  • return出现在其他函数中时,它表示结束该函数的执行,并将控制权返回给调用者。如果该函数有返回值类型(非void),则return语句后面需要跟上相应的返回值。

exit是C标准库中的一个函数,用于终止当前进程的执行。它的行为大致如下:

  1. 刷新缓存区exit函数会首先刷新所有输出缓冲区(如stdoutstderr),确保所有待写数据都被发送到它们的目的地(如终端或文件)。

  2. 调用atexit注册的退出函数:在程序正常终止时(即不是通过abort_exit或接收到致命信号而终止),exit会按照它们被注册的顺序逆序调用所有通过atexit函数注册的清理函数。这些函数用于执行必要的清理工作,如释放资源、关闭文件等。

  3. 执行_exit(或类似):实际上,exit函数并不直接调用_exit(注意,_exit是POSIX标准中的一个函数,而_Exit是C11标准中引入的宏,它们的行为相似但略有不同)。但是,在完成了上述步骤之后,exit函数会以一种方式终止进程,这种方式在效果上与_exit_Exit相似,即直接终止进程而不返回到调用者,并且不会执行任何进一步的清理工作(因为exit已经完成了这些工作)。然而,从exit_exit(或类似机制)的具体实现细节是库依赖的,并且对于大多数用户来说是不可见的。

  4. 向操作系统报告退出状态:最后,exit会将传递给它的状态码作为进程的退出状态报告给操作系统。这个状态码可以被父进程捕获,用于判断子进程的终止原因。

需要注意的是,虽然exit_exit/_Exit都用于终止进程,但它们的行为有所不同。exit会执行清理工作(如刷新输出缓冲区、调用atexit注册的函数),而_exit/_Exit则不会。因此,在大多数情况下,应该使用exit来终止程序,除非有特殊的需求需要绕过这些清理步骤。

_exit    系统调用

    void _exit(int status);
    功能:
        让进程退出,不刷新缓存区
    参数:
        status:进程退出状态
    返回值:
        缺省

atexit

    int atexit(void (*function)(void));
    功能:
        注册进程退出前执行的函数
    参数:
        function:函数指针
            指向void返回值void参数的函数指针
    返回值:
        成功返回0
        失败返回非0

    当程序调用exit或者由main函数执行return时,所有用atexit
    注册的退出函数,将会由注册时顺序倒序被调用
 

进程空间的回收

1、wait ()函数

wait/waitpid

    pid_t wait(int *status);
功能:该函数可以阻塞等待任意子进程退出
      并回收该进程的状态。
      一般用于父进程回收子进程状态。

参数:status 进程退出时候的状态
      如果不关心其退出状态一般用NULL表示
      如果要回收进程退出状态,则用WEXITSTATUS回收。

返回值:成功 回收的子进程pid
        失败 -1;
  这个函数会阻塞父进程的执行,直到一个子进程结束。当子进程结束时,wait() 函数会收集子进程的结束状态,并允许父进程继续执行。

宏的功能
  • WIFEXITED(status):检查子进程是否通过调用 exit() 或从 main() 函数返回而正常退出。如果是,返回非零值(真)。

  • WEXITSTATUS(status):如果 WIFEXITED() 返回真,则 WEXITSTATUS(status) 宏返回子进程的退出状态。这个状态是子进程通过 exit() 函数的参数或 main() 函数的返回值提供的。

  • WIFSIGNALED(status):检查子进程是否因为接收到一个未被捕获的信号而终止。如果是,返回非零值(真)。

  • WTERMSIG(status):如果 WIFSIGNALED() 返回真,则 WTERMSIG(status) 宏返回导致子进程终止的信号的编号。

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

int main(int argc, char *argv[])
{
    pid_t ret = fork();
    if(ret>0)
    {
        //father
            printf("father is %d   pid %d ,ppid:%d  \n",a,getpid(),getppid());
            int status;
            pid_t pid = wait(&status);
            if(WIFEXITED(status))// 代表子进程正常结束
            {
                //正常结束的子进程,才能获得退出值
                printf("child quit values %d\n",WEXITSTATUS(status));
            }
            if(WIFSIGNALED(status))//异常结束
            {
                printf("child unnormal signal num %d\n", WTERMSIG(status));
            }

            printf("after wait, %d\n",status);

    }
    else if(0 == ret)
    {
        //child
            printf("child a is %d pid:%d ppid:%d\n",a,getpid(),getppid());
            sleep(5);
            printf("child terminal\n");
            exit(50);    //调用 exit() 函数时,程序会立即终止,并返回给操作系统一个整数状态码
    }
    else 
    {
        perror("fork error\n");
        return 1;
    }
   
    return 0;
}

2.waitpid

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

waitpid(-1,status,0)=wait(status);
    < -1 回收指定进程组内的任意子进程
    -1 回收任意子进程,组内外
    0 回收和当前调用waitpid一个组的所有子进程,组内
    > 0 回收指定ID的子进程
     waitpid (-1,a,0)  == wait(a);
     status 子进程退出时候的状态,
              如果不关注退出状态用NULL;
      options 选项:
                  0  表示回收过程会阻塞等待
                WNOHANG 表示非阻塞模式回收资源。
    返回值:成功 返回接收资源的子进程pid
        失败  -1
        0,

练习:waitpid()函数等待子进程结束,同时获取子进程的退出状态

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    pid_t ret = fork();
    if(ret>0)
    {
        //father
        printf("father  pid %d ,ppid:%d  \n",getpid(),getppid());
        int status;
        while(1)
        {
            pid_t pid = waitpid(ret,&status, WNOHANG);
            if(ret == pid)
            {
                if(WIFEXITED(status))// 代表子进程正常结束
                {
                    //正常结束的子进程,才能获得退出值
                    printf("child quit values %d\n",WEXITSTATUS(status));
                }
                if(WIFSIGNALED(status))//异常结束
                {
                    printf("child unnormal signal num %d\n", WTERMSIG(status));
                }
                break;
            }
            else if(0 == pid)
            {
                printf("子进程未结束,稍后在试\n");
            }

        }
        printf("after wait, %d\n",status);

    }
    else if(0 == ret)
    {
        //child
            printf("child  pid:%d ppid:%d\n",getpid(),getppid());
            sleep(5);
            printf("child terminal\n");
            exit(50);
    }
    else 
    {
        perror("fork error\n");
        return 1;
    }
    return 0;
}

练习: 设计一个多进程程序,用waitpid函数指定回收
    其中的某个进程资源并将其状态打印输出。
    其他的进程都以非阻塞方式进行资源回收

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    int i = 0 ;
    pid_t ret[5]={0};
    printf("father  pid %d ,ppid:%d  \n",getpid(),getppid());
    for(i = 0 ;i<5;i++)
    {
        ret[i] = fork();
        if(ret[i]>0)
        {
            //father

        }
        else if(0 == ret[i])
        {
            //child
            printf("child  pid:%d ppid:%d\n",getpid(),getppid());
            sleep(rand()%5);
            exit(1);
        }
        else 
        {
            perror("fork error\n");
            return 1;
        }
    }
    int status;
    while(1)
    {
        pid_t pid = waitpid(ret[2],&status, WNOHANG);
        if(ret[2] == pid)
        {
            if(WIFEXITED(status))// 代表子进程正常结束
            {
                //正常结束的子进程,才能获得退出值
                printf("child quit values %d\n",WEXITSTATUS(status));
            }
            if(WIFSIGNALED(status))//异常结束
            {
                printf("child unnormal signal num %d\n", WTERMSIG(status));
            }
            printf("father recycle success, pid :%d\n",pid);
            break;
        }
        else if(0 == pid)
        {
            printf("子进程未结束,稍后在试\n");
            sleep(1);
        }

    }
    printf("after wait, %d\n",status);
    return 0;
}

exec族

     用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),
子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的
用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建
新进程,所以调用exec前后该进程的id并未改变。其实有六种以exec开头的函数,统称exec函数

exec 函数族是用于执行新程序的一组函数。这些函数将当前进程的映像替换为一个新的程序,但进程ID(PID)保持不变。

1. execv()

  • 原型int execv(const char *path, char *const argv[]);
  • 功能execv函数通过指定的文件路径(path)来执行一个程序,并将参数列表(argv)传递给该程序。argv数组的第一个元素通常是被执行程序的名称,之后的元素是传递给程序的参数,数组以NULL指针结束。
  • 返回值:如果execv调用成功,它不会返回;如果调用失败,则返回-1,并设置errno以指示错误原因。

2. execvp()

  • 原型int execvp(const char *file, char *const argv[]);
  • 功能execvp函数类似于execv,但它会在环境变量PATH指定的目录列表中搜索file指定的程序。如果找到,则执行该程序,并将argv数组作为参数传递。
  • 返回值:与execv相同,成功时不返回,失败时返回-1并设置errno

3. execl()

  • 原型int execl(const char *path, const char *arg, ..., /* (char *) NULL */);
  • 功能execl函数直接接受一个可变数量的参数来指定要执行的程序路径(path)和传递给该程序的参数列表。参数列表以NULL结束(注意,由于这是变参函数,NULL并不作为参数显式传递,而是通过函数参数列表的结束来隐式表示)。
  • 返回值:与execv相同,成功时不返回,失败时返回-1并设置errno

4. execlp()

  • 原型int execlp(const char *file, const char *arg, ..., /* (char *) NULL */);
  • 功能execlp函数结合了execlexecvp的特点。它接受一个可变数量的参数来指定要执行的程序名称(file)和传递给该程序的参数列表,但它会在环境变量PATH指定的目录列表中搜索该程序。
  • 返回值:与execv相同,成功时不返回,失败时返回-1并设置errno

1),前4个使用路径名作为参数,后面两个使用文件名做参数
    当filename中,含有/时视为路径名,否则就按PATH变量,在指定目录下查找可执行文件。

2)相关的参数表传递
    l表示list,v表示vector
    execl,execlp,execle,需要将参数一个一个列出,并以NULL结尾。
    execv,execvp,execve,需要构造一个参数指针数组,然后将数组的地址传入。

    3)以e结尾的函数,可以传入一个指向环境字符串的指针数组的指针。其他未指定环境变量,使用父进程继承过来的。
execve 是真正的系统调用
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错
则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。

练习:编译生成一个可执行文件”aaa“,用exec使用

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(int argc, const char *argv[])
{
	execl("aaa","aaa","1","2",NULL);   //在同一个目录可以不加路径(会搜索当前路径)
	//execlp("./aaa","aaa","1","2",NULL);        //要给路径(搜索系统路径)
	char *s[]={"aaa","1","2",NULL};
	//execv("aaa",s);
	//execvp("./aaa",s);
	printf("error\n");
	exit(1);
	
	return 0;
}

  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值