【Linux】进程控制

进程创建

我们在写C/C++代码时,可以用 fork() 函数进行子进程的创建,

下面就对fork()函数进行简单的介绍。

简单认识一下fork()函数

NAME
	fork - create a child process
    //fork - 创建一个子进程

SYNOPSIS
	#include <unistd.h> //头文件
	pid_t fork(void);   //函数原型
       
RETURN VALUE
	On success, the PID of the child process is returned in the parent, and 0 is returned in the child.  On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.
    //如果成功,在父进程中返回子进程的PID,在子进程中返回0。失败时,在父进程中返回-1,不创建子进程,并适当地设置errno。
       
NOTES
	Under  Linux, fork() is implemented using copy-on-write pages, so the only penalty that it incurs is the time and memory required to duplicate the parent's page tables, and to create a unique task structure for the child.
    //在Linux下,fork()是使用对数据分页的写时拷贝实现的,所以它唯一的代价是复制父进程的页表以及为子进程创建独特的任务结构所需的时间和内存。

为什么fork()会有两个返回值

我们知道fork会创建子进程,所以它内是怎样一套执行逻辑呢?

首先会给子进程分配新的内存块和内核数据结构,

然后会将父进程部分数据结构内容拷贝给子进程,

准备工作做好了,进程已经创建出来了,

再将子进程添加到系统进程列表当中,

一切工作做完,最后return返回。

所以,在return返回之前就已经创建好了子进程!

由于在复制时复制了父进程的堆栈段,

所以两个进程都停留在fork函数中,等待返回,

因此fork函数会返回两次,

一次是在父进程中返回,另一次是在子进程中返回,

这两次的返回值是不一样的。

至于为什么给父进程返回子进程的PID,

给子进程返回0,

一个很简单的道理,

一个儿子只能有一个亲生父亲,

而一个父亲可以有若干个子女,

放在进程上同样适用,

所以父进程需要子进程的PID来唯一标识子进程。


fork通过写时拷贝的方式创建子进程

在NOTE部分提到fork() is implemented using copy-on-write pages

调用fork之后,进程地址空间有两份,页表也有两份,

但内存中实实在在的数据和代码只有一份,

当父子进程有一个想要对数据进行写入时,

此时操作系统会在内存上开一块空间,

然后把需要写入的数据拷贝过来,

想要修改的一方修改对应的页表映射,

然后在新的内存空间进行读写,

从而保证了父子进程的独立性,

有在一定程度上节省了空间,

这就是所谓的写时拷贝。

image-20230214221543551


进程终止

进程退出时会销毁PCB以及进程地址空间,

释放掉页表以及其中的各种映射关系,

代码段与数据段所占用的空间也要被还给系统。

当然,进程终止还要将进程执行情况报告给父进程,

如果父进程迟迟不接收,

子进程可能就处于一个僵尸状态(Zombie),

而进程是怎么返回执行情况的呢,

下面就介绍一下进程退出码。


进程退出码

我们平时写main函数的时候经常写return 0;

但是这个return的0有什么意义呢?可以return别的数吗?

这就不得不介绍一下进程退出码的概念了。

当一个进程退出的时候,

可能是因为正常代码执行结束退出了,

也可能是因为发生了一些错误导致进程退出了,

作为管理进程的操作系统,

他需要知道进程是为什么结束的,

就像老板给员工下发了任务,

员工最终是要像老板汇报任务的完成状况的,

是成功完成了,还是遇到了技术难题…

而进程退出码就是充当这个结束信息的。

我们写main函数时通常写return 0,但实际上也能随便return 其他数字,

在linux下可以通过指令echo $?查看最近一个进程的退出码:

image-20230214223929140

只不过我们不关心它,所以可以随便写。

但是退出码只是一个数字,并不能直观地反映出问题所在,

所以一般而言退出码都有对应的文字描述,

如果是我们自己写的程序,

我们可以自己规定对应信息,

比如0对应程序正常执行结束且执行结果正确,

1对应程序正常执行结束但执行结果不正确。

当然对于系统提供的一套程序,

都有系统自己的一套映射关系,

我们可以通过C语言函数strerror()查看某一进程退出码对应的信息:

NAME
	strerror - return string describing error number

SYNOPSIS
	#include <string.h>
	char *strerror(int errnum);

RETURN VALUE
    The  strerror() function returns a pointer to a string that describes the error code passed in the argument errnum, or an "Unknown error nnn" message if the error number is unknown.

可以用下面的代码来查看所有进程码对应的信息:

#include <stdio.h>
#include <string.h>

int main()
{
    for (int i = 0; i <= 133; i++)
        printf("%d : %s\n", i, strerror(i));
    return 0;
}

至于为什么打印范围是0 ~ 133,

是因为进程退出码就提供了这么多:

image-20230214225813661

image-20230214225834062

下面简单测试一下:

image-20230214225924777

echo $?会打印最近一个进程的退出码,

显然最近一个进程是ls,它的退出码是2,

2对应的错误信息看上面的图就是No such file or dierctory

而ls的报错信息正好也是这个,所以这就完全对应上了。

不过需要注意,

只有当进程是因为正常运行结束,无论执行结果错误与否,

这时的返回值才是有意义的。

当进程运行时崩溃了的话,

此时的返回码是无意义的:

image-20230215133534557

image-20230215133556869

此时进程一般是收到了退出信号,在下面的进程等待部分会看一下。


进程退出的方式

进程正常的退出方式有三种,

main函数执行结束正常退出,

调用 函数exit() 退出,

调用 系统调用接口_exit() 退出。

当然,进程也有不正常的退出方式,

什么调用abort啦、收到退出信号啦等等,

不过这里不讲。

main函数正常执行结束进程退出没什么好说的,

不过需要注意一下,

通过main函数return+数字返回进程退出码的方式,

其实和exit(退出码)的退出方式其实是一样的,

因为调用main的运行时函数会将main的返回值当做 exit的参数。

所以下面着重介绍对比一下通过调用 exit()_exit() 的退出方式。


exit()和_exit()

NAME
	exit - cause normal process termination
	//exit - 导致正常进程终止

SYNOPSIS
	#include <stdlib.h>
	void exit(int status);
	
DESCRIPTION
	The exit() function causes normal process termination and the value of status & 0377 is returned to the parent (see wait(2)).
	//exit()函数导致正常的进程终止,status & 0377的值返回给父进程(参见wait(2))
NAME
	_exit - terminate the calling process

SYNOPSIS
	#include <unistd.h>
	void _exit(int status);

DESCRIPTION
	The function _exit() terminates the calling process "immediately".  Any open file descriptors belonging to the process are closed; any children of the process are inherited by process 1, init, and the process's parent is sent a SIGCHLD signal.
	The value status is returned to the parent process as the process's exit status, and can be collected using one of the wait(2) family of calls.
    //函数_exit()“立即”终止调用进程。属于该进程的任何打开的文件描述符都被关闭;进程的所有子进程都被进程1继承,并且进程的父进程被发送一个SIGCHLD信号。
	//值状态作为进程的退出状态返回给父进程,并且可以使用wait(2)系列调用之一来收集。

这里还是先强调一下,

exit()是库函数,_exit()是系统调用接口。

看描述两个进程貌似没什么区别,

都是终止进程并将参数作为退出码返回给父进程。

但是看下面两段代码的运行结果看一下:

#include <unistd.h>
#include <stdlib.h>

int main()
{
    printf("hello world"); //这里并没有换行符,所以不会刷新缓冲区
    exit(-1);
    return 0;
}
#include <unistd.h>
#include <stdlib.h>

int main()
{
    printf("hello world"); //这里并没有换行符,所以不会刷新缓冲区
    _exit(-1);
    return 0;
}

这是第一段代码的运行结果:

image-20230215143203238

这是第二段代码的运行结果:

image-20230215143256012

可以发现第二段代码竟然没有打印。

这里多提一个I/O缓冲的概念:

I/O缓冲是指在内存里开辟一块区域里存放的数据是用来接收用户输入和用于计算机输出的数据以减小系统开销和提高外设效率。

所以printf要打印的数据是存在缓冲区里的,

想要输出得刷新缓冲区。

知道了这一点,我们就可以确认,

exit退出进程时会刷新缓冲区

_exit退出进程时不会刷新缓冲区

其实这也侧面说明了一点,

这里C语言的的I/O缓冲区是用户级别的

exit()是C语言函数,是经过封装的用户操作接口,

可以对用户层面的数据进行操作;

而_exit()是系统调用接口,

系统调用接口向下是操作系统,向上是用户,

所以_exit()无法对用户层面的数据进行操作。

总结一下,exit()实际上最后也会调用_exit()退出进程,

但在这之前还封装了一些功能:

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

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

  3. 调用_exit

而_exit()就很单纯了,就是简单的退出进程。


进程等待

进程状态一文中有提到存在着一种特殊的进程状态 —— 僵尸状态(zombie)。

僵尸状态是指子进程已经运行结束准备返回运行结果,

通俗来讲就是子进程已经完成任务等待父进程的回收。

内核维护关于僵尸进程的最小信息集(PID、终止状态、资源使用信息),

以便允许父进程稍后执行等待以获取关于子进程的信息。

只要僵尸进程没有通过等待从系统中删除,

它就会占用内核进程表中的一个槽,

如果这个表已满,就不可能再创建其他进程。

如果父进程终止,那么它的“僵尸”子进程(如果有)将被init(8)采用,

init(8)将自动执行等待以删除僵尸进程,不过这都是后话了。

那么父进程如何接收子进程的执行信息好让操作系统回收子进程呢?

这就是进程等待要解决的问题。


进程等待方法 – wait()和waitpid()

NAME
	wait, waitpid - wait for process to change state

SYNOPSIS
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *status);
    pid_t waitpid(pid_t pid, int *status, int options);

DESCRIPTION
	Both of the system calls are used to wait for state changes in a child of the calling process, and obtain information about the child whose state has changed. 
    //这两个系统调用都用于等待调用进程的子进程的状态更改,并获取关于状态已更改的子进程的信息。
        
	A state change is considered to be: the child terminated; the child was stopped by a signal; or the child was resumed by a signal.   
    //状态改变被认为是:子进程终止;一个信号让子进程停了下来;或者这个子进程被一个信号打断了。
        
	In the case of a terminated child, performing a wait allows the system to release the resources associated with the child; if a wait is not performed, then the terminated child remains in a "zombie" state.
	//在终止子进程的情况下,执行等待允许系统释放与子进程相关的资源;如果没有执行等待,则终止的子进程将保持“僵尸”状态。
        
	The wait() system call suspends execution of the calling process until one of its children terminates. The call wait(&status) is equivalent to: waitpid(-1, &status, 0);
	//wait()系统调用挂起调用进程的执行,直到它的一个子进程终止。呼叫等待(&status)等价于:waitpid(-1, &status, 0);

	The waitpid() system call suspends execution of the calling process until a child specified by pid argument has changed state. By default, waitpid() waits only for terminated children, but this behavior is modifiable via the options argument.
	//waitpid()系统调用将挂起调用进程的执行,直到pid参数指定的子进程改变了状态。默认情况下,waitpid()仅等待终止的子节点,但此行为可通过options参数修改
        

RETURN VALUE
	wait(): on success, returns the process ID of the terminated child; on error, -1 is returned.
    //wait():如果成功,返回终止子进程的进程ID;如果出错,返回-1。

	waitpid(): on success, returns the process ID of the child whose state has changed; if WNOHANG was specified and one or more child(ren) specified by pid exist, but have not yet changed state, then 0 is returned. On error, -1 is returned.
    //waitpid():如果成功,返回状态发生变化的子进程的进程ID;如果指定了WNOHANG,并且pid指定的一个或多个子进程存在,但尚未改变状态,则返回0。如果出错,返回-1。

需要注意,这里的 wait() 和 waitpid() 都是系统调用接口

不过C语言也封装了自己的 wait() 和 waitpid() 方法,大同小异。


status参数解释

两个接口的参数都有一个status,传进来的是个指针,

status其实是个输出型参数

会在函数内部将子进程的状态写入到status中,

所以就可以通过传进来的指针对父进程中定义的status进行修改,

从而将子进程的退出信息传回给父进程。

当然,如果不关心子进程的退出信息,参数也可以传NULL

下面就用 wait() 来简单测试一下:

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

int main()
{
    pid_t id = fork();
    assert(id != -1);
    
    if (id == 0)
    {
        //child
        exit(100);    
    }
    
    int status = 0;
    wait(&status);
    printf("status : %d\n", status);
    
    return 0;
}

子进程上来就退出,然后父进程调用wait()等待子进程结束。

运行结果如下:

image-20230216154546751

但是打印出来的status并不是子进程设置的退出码100,

这是因为status有自己的位图结构:

image-20230216154800836

虽然他是一个32位整型,但是这里只用到了它的低的16个bit位,

其中前七个bit位,也就是0~6位,存放终止信号信息,

第八位是core dump标志,用于表示进程终止时是否进行了核心转储,

次第八位,也就是8~15位,存放退出码信息。

当进程正常执行完毕,无论正确与否,都属于正常退出,

此时就将退出码写入status的8~15位,对于不关系的都设置成0,

当进程执行时出现错误收到退出信号,

此时会将信号信息写入status的前七位。

所以如果想看退出码和退出信号,

需要对status进行一些位运算:

exit_code = (status >> 8) & 0xff, exit_signal = status & 0x7f

所以修改一下代码:

int main()
{
    pid_t id = fork();
    assert(id != -1);
    
    if (id == 0)
    {
        //child
        exit(100);    
    }
    
    int status = 0;
    wait(&status);
    
    int exit_code = (status >> 8) & 0xff, exit_signal = status & 0x7f;
    printf("exit_code : %d\texit_signal : %d\n", exit_code, exit_signal);
    
    return 0;
}

运行结果如图:

image-20230216163712056

也可以让子进程执行死循环,然后用 kill -9 终止子进程看一下退出信号:

int main()
{

    pid_t id = fork();
    assert(id != -1);
    
    if (id == 0)
    {
        //child
        while(1)
        {
            printf("我是子进程, pid = %d, ppid = %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    
    int status = 0;
    wait(&status);
    int exit_code = (status >> 8) & 0xff, exit_signal = status & 0x7f;
    printf("exit_code : %d\texit_signal : %d\n", exit_code, exit_signal); 
    
    return 0;

运行结果:

image-20230216164548072

像父进程如果调用 wait() 后,

父进程会一直卡在这儿,直到子进程退出。

但是像 waitpid() 还可以通过控制 options 参数使父进程以其他方式等待子进程退出,

而不是傻傻的等着啥也不干。

所以 waitpid() 不是非得等子进程退出才能返回的,

那子进程还未推出的时候 waitpid() 要返回,

此时的 status 应该写入些什么呢?

官方提供了如下几种宏:

WIFEXITED(status) : returns true if the child terminated normally. (如果子进程正常终止,则返回true)

所以我们可以用这个宏来判断子进程是否正常运行结束。

WEXITSTATUS(status) : returns the exit status of the child. This macro should be employed only if WIFEXITED returned true. (返回子进程的退出状态。只有当WIFEXITED返回true时,才应该使用这个宏。)

当子进程正常退出,它其实就是status所包含的退出码。

WIFSIGNALED(status) : returns true if the child process was terminated by a signal. (如果子进程被信号终止,则返回true。)

可以用这个宏来判断子进程是否是被信号打断而退出的。

WTERMSIG(status) : returns the number of the signal that caused the child process to terminate. This macro should be employed only if WIFSIGNALED returned true. (返回导致子进程终止的信号的编号。只有当WIFSIGNALED返回true时,才应该使用这个宏。)

跟上一个宏配对,他俩跟前面俩就是相辅相成的。

WCOREDUMP(status) : returns true if the child produced a core dump. This macro should be employed only if WIFSIGNALED returned true. (如果子进程产生了核心转储,则返回true。只有当WIFSIGNALED返回true时,才应该使用这个宏。)

同样是在进程被信号打断的情况下使用,来判断子进程是否产生了核心转储。

这里就列举这几个,因为不常见,所以我也只是想了解一下才列举的,

更详细的可参见man手册。


waitpid()的pid参数

pid < -1 : meaning wait for any child process whose process group ID is equal to the absolute value of pid. (等待任何进程组ID等于pid绝对值的子进程)

应该没人用这么吧。。

pid = -1 : meaning wait for any child process. (等待任何子进程)

pid传-1, 如果父进程是阻塞式等待的话,

和调用wait就完全一样了。

pid = 0 : meaning wait for any child process whose process group ID is equal to that of the calling process. (等待与父进程进程组ID相等的子进程)

一个进程除了进程ID外,还有一个进程组ID。

pid > 0 : meaning wait for the child whose process ID is equal to the value of pid. (意思是等待进程ID等于pid值的子进程)

最常用的打开方式。

不过,如果传过来的pid对应的子进程不是父进程的子进程,

或者说是一个不存在的进程,

那么waitpid()就会调用失败。


waitpid()的options参数 - 阻塞和非阻塞

options的值通常是0或官方提供的一些宏

下面就简单介绍一下:

0 : 父进程以阻塞方式等待子进程运行结束。子进程运行结束后返回子进程的pid。

WNOHANG : 父进程以非阻塞的方式访问子进程,如果结束了就正常返回子进程的pid,如果子进程还未结束就返回0,这点在waitpid()的返回值部分有介绍。

下面就对这两个参数的使用进行一下简单的介绍。

前面在介绍接口的DESCRIPTION部分提到过下面两种调用方式是完全等价的:

wait(&status) <=> waitpid(-1, &status, 0)

所以阻塞状态很好理解,

就是父进程卡在这儿,

一直等到子进程结束才继续向下执行,

来段代码感受一下:

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

int main()
{
    pid_t id = fork();
    assert(id != -1);

    if (id == 0)
    {
        //child
        int child_count = 5;
        while (child_count)
        {
            printf("child_count=%d pid=%d\n", child_count, getpid());
            child_count--;
            sleep(1);
        }
        exit(10);
    }

    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if (ret > 0)
    {
        printf("wait success! exit_code=%d exit_signal=%d\n", (status>>8)&0xff, status&0x7f);
    }

    int parent_count = 5;
    while (parent_count)
    {
        printf("parent_count = %d\n", parent_count);
        parent_count--;
        sleep(1);
    }

    return 0;
}

运行结果如下:

进程控制1

这就是阻塞等待。

相对应的就是非阻塞等待。

非阻塞等待顾名思义,

父进程通过waitpid()一键查询子进程的状态,

发现子进程还没有结束,

父进程也不等它,

直接回过头来继续干自己的事。

但是问题来了,

如果父进程只调用了一次waitpid,

发现子进程没结束就继续向下执行自己的代码,

那这样父进程到结束都没能再次查询子进程的状态,

相应的,子进程还是成了所谓的僵尸进程。

所以非阻塞等待的正确打开方式应该是不断查询子进程的状态

如果子进程结束了就不再访问子进程,

如果子进程没结束就先做一会儿自己的事,

过一会再去看看子进程执行个啥样,

而这种不断查询子进程的等待方式,就叫做轮询等待

阻塞等待option参数传0

那么非阻塞等待option参数就是传WNOHANG

轮询模板如下:

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

#define NUM 10

typedef void (*func_t)(); //函数指针

func_t handlerTask[NUM];  //函数指针数组

//样例任务
void task1()
{
    //...
}
void task2()
{
    //...
}
void task3()
{
    //...
}

//加载任务
void loadTask()
{
    memset(handlerTask, 0, sizeof(handlerTask));
    handlerTask[0] = task1;
    handlerTask[1] = task2;
    handlerTask[2] = task3;
}

int main()
{
    pid_t id = fork(); //创建子进程
    assert(id != -1);  //判断是否创建子进程成功
    
    //子进程执行流
    if(id == 0)
    {
		//...
        exit(0);
    }

    //父进程执行流
    loadTask();  //加载任务
    int status = 0;
	//开始轮询等待
    while(1)
    {
        pid_t ret = waitpid(id, &status, WNOHANG); //非阻塞等待
        
        if(ret == 0)  //调用成功且子进程没有结束
        {
            printf("wait done, but child is running...., parent running other things\n");
            for(int i = 0; handlerTask[i] != NULL; i++)
            {
                handlerTask[i](); //采用回调的方式,执行我们想让父进程在空闲的时候做的事情
            }
        }
        
        else if(ret > 0) //调用成功且子进程结束了,可以打印退出信息并跳出轮询状态
        {
            printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
            break;
        }
        
        else  //waitpid调用失败
        {
            printf("waitpid call failed\n");
            break;
        }
        
        sleep(1);
    }
    
    return 0;
}

进程替换

我们已经可以创建一个子进程了,

但是我们创建子进程之后可以做什么呢?

一方面我们可以让子进程执行父进程的一部分代码,

另一方面,我们还可以让子进程执行其他程序。

这就是下面要讨论的进程替换。


引入

初识环境变量一文中我们了解到,

像ls、pwd等命令本质上是一个个的C语言代码编译形成的可执行程序,

那么在我们自己写的C语言程序中能否调用它们呢?

请看下面的代码:

int main()
{
    printf("process is running...\n");
    
    pid_t id = fork();
    assert(id != -1);  //判断子进程是否创建成功
    
    if (id == 0)  //成功创建子进程
    {
        execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        printf("I'm child process\n");
        exit(-1);
    }
    
    //等待子进程返回
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if (ret > 0)
       printf("wait success! exit_code=%d exit_signal=%d\n", (status>>8)&0xff, status&0x7f);

    printf("running done\n");
    return 0;
}

运行结果如下:

image-20230217195001566

发现子进程确实去调用ls命令了,

而且子进程的打印语句也没有执行。

这到底是怎么一回事呢?


进程替换的原理

上面调用exec函数使子进程执行了另一个程序,

这种执行方式叫做进程的程序替换。

当进程调用一种exec函数时,

该进程的用户空间代码和数据完全被新程序替换,

从新程序的启动例程开始执行。

这是调用**fork()**创建完子进程时的样子:

image-20230217210341372

这是调用exec替换后的样子:

image-20230217210642099

待替换的程序会从硬盘加载到内存,

新程序的代码和数据会覆盖子进程的代码和数据,

本来子进程和父进程是共享一份代码和数据的,

但因为进程之间的独立性,这时会进行写时拷贝,

重新开辟一块空间将新程序的代码和数据写入,

然后做一系列进程启动工作,

从而完成进程替代。

紧接着,原来的子进程就变成新进程运行了。

不过,调用exec并不创建新进程

所以调用exec前后该进程的id并未改变。


进程替换函数

C语言一共提供了六个进程替换函数,

它们都是以exec开头,所以统称exec函数,

并且它们的用法十分接近,

下面对它们进行简单的讲解。

NAME
	execl, execlp, execle, execv, execvp, execvpe - execute a file

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


RETURN VALUE
	The exec() functions return only if an error has occurred, the return value is -1.
    //exec()函数只在发生错误时返回。返回值为-1,并设置errno以指示错误。
    //其实很好理解,如果成功调用那剩下的代码就没什么事了,就会被完全替换成另一套代码。
	//还有就是,如果失败了,会回来执行原来的代码。

这些函数的后缀由l、v、p、e组成,四个字符其实各有千秋:

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

下面解释一下各个参数的含义。

首先是const char *path

这个其实就是我们要替换的程序的路径,

比如我们要替换的程序是ls

ls的完整绝对路径是**/usr/bin/ls**,

所以path参数就要传**“/usr/bin/ls”**:

execl("/usr/bin/ls", const char *arg, ...)

这个用绝对路径和相对路径都没问题。

比如要替换跟父进程所对应的可执行程序在同一目录下的程序myexe就可以这么传:

execl("./myexe", const char *arg, ...)

有的函数没有传路径,而是传的文件名const char *file

这些函数都有一个共同的特点:函数名中都有p

意思是我只需要传一个文件名,

它会自动在环境变量PATH包含的路径下去寻找。

比如我还是要调用ls,但ls的路径是包含在PATH中的,

所以我用带p的函数去替换就可以这么调

execlp("ls", const char *arg, ...)

有的函数第二个参数是const char *arg, ...

这些函数都有一个共同的特点:函数名中都有l

这其实是个可变参数列表,

就和我们经常用的printfscanf一样,

传的参数数量是可以不固定的。

可变参数列表要传的参数其实就是程序的调用方式。

什么是调用方式呢?

我们在命令行调用指令的时候会加各种选项,

这其实就是我们调用指令的方式。

比如我要调用ls -a -l

那么这个参数就可以传:execl("/usr/bin/ls", “ls”, "-a", "-l", NULL)

不过这样是没有语法高亮的,

想要语法高亮就可以这么调:execl("/usr/bin/ls", “ls”, "-a", "-l", "--color=auto", NULL)

不过有一点需要注意,参数列表一定要以NULL结尾。

有的函数第二个参数是char *const argv[]

这些函数都有一个共同的特点:函数名中都有v

C语言功底深厚的小伙伴应该一眼就能看出这是一个指针数组,

它的每一个元素都是char*,

那到这儿应该就看明白了,

其实就是把上面的可变参数列表以指针数组的方式传入,

例如我想这么调用execl("/usr/bin/ls", “ls”, "-a", "-l", "--color=auto", NULL)

不用可变参数列表的话就可以定义一个指针数组:

char *const argv_[] = {"ls", "-a", "-l", "--coloc=auto", NULL}

注意一定要以NULL结尾

然后就可以这么调用:

execv("/usr/bin/ls", argv_)

有的函数还有第三个参数char *const envp[]

这些函数都有一个共同的特点:函数名中都有e

envp也是一个指针数组,

只不过它的每个元素是指向环境变量的。

这个其实是使用我们自定义的环境变量,

这就需要我们自己去定义:

char *const envp_[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}

如果是以这种方法去使用的,那可就要注意了。

这里先拓展一个问题,

main函数和exec函数谁先执行谁后执行呢?

对于被替换的进程而言,显然是exec先执行完毕,

才开始main函数的执行。

那么问题是,main函数也有参数,它的三个参数从哪来呢?

就是exec为它提供的!

默认情况下exec会继承父进程的环境变量传承给main,

但是一旦我们使用了自定义的环境变量,

main函数就接收不到系统的环境变量了!

以我们上面定义的那个envp_举例子,

将它传进去之后替换后的进程的地址空间就只有PATH和TERM两个环境变量了。

不过没关系,还有一个全局的char** environ,

但是这样的话使用我们定义的和使用系统的全局变量就不统一了,

一个很好的解决方法是在父进程中先用putenv()导入我们的环境变量,

然后参数再传environ。

所以我们既可以这么调用:

execle("/usr/bin/ls", "ls", NULL, envp_)

也可以这么调用:

extern char **environ;
putenv((char*)"MYENV=4443332211");
execle("/usr/bin/ls", "ls", NULL, environ);

以上就是对各个函数的介绍,想必已经有了一定了解。

下面是各个函数的使用模板:

#include <unistd.h>

int main()
{
    char *const argv_[] = {"ls", "-al", NULL};
    
    char *const envp_[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
    
    execl("/bin/ls", "ls", "-al", NULL);

    execlp("ls", "ls", "-al", NULL);

    execle("ls", "ls", "-al", NULL, envp_);
    
    execv("/usr/bin/ls", argv_);

    execvp("ls", argv_);

    execvpe("ls", argv_, envp_);
    
    return 0;
}

以上介绍了C语言库里提供的六种进程替换函数,

实际上还有一个更底层的系统调用接口:

int execve(const char *filename, char *const argv[], char *const envp[])

它们的关系如图所示:

image-20230218005553527


进程替换的使用

进程替换是用可执行程序来替换的,

这个可执行程序可以是C语言代码编译形成的可执行程序,

也可以是C++代码编译形成的可执行程序,

也可以是可执行的Python程序,

也可以是可执行的shell程序…

所以这样用是完全可行的:

//mybin是当前目录下c++代码编译生成的可执行文件
execl("./mybin", "mybin", NULL);

//mypy.py是当前目录下的一个可执行python程序
execl("./mypy.py", "mypy.py", NULL);

//myshell.sh是当前目录下的一个可执行shell程序
execl("./myshell.sh", "myshell.sh", NULL);
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LeePlace

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值