【Linux】进程控制
文章目录
- 进程创建
- 简单认识一下fork()函数
- 为什么fork()会有两个返回值
- fork通过写时拷贝的方式创建子进程
- 进程终止
- 进程退出码
- 进程退出的方式
- exit()和_exit()
- 进程等待
- 进程等待方法 -- wait()和waitpid()
- status参数解释
- waitpid()的pid参数
- waitpid()的options参数 - 阻塞和非阻塞
- 进程替换
- 引入
- 进程替换的原理
- 进程替换函数
- 进程替换的使用
进程创建
我们在写C/C++代码时,可以用 fork() 函数进行子进程的创建,
下面就对fork()函数进行简单的介绍。
简单认识一下fork()函数
NAMEfork - create a child process//fork - 创建一个子进程SYNOPSIS#include <unistd.h> //头文件pid_t fork(void); //函数原型RETURN VALUEOn 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。NOTESUnder 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之后,进程地址空间有两份,页表也有两份,
但内存中实实在在的数据和代码只有一份,
当父子进程有一个想要对数据进行写入时,
此时操作系统会在内存上开一块空间,
然后把需要写入的数据拷贝过来,
想要修改的一方修改对应的页表映射,
然后在新的内存空间进行读写,
从而保证了父子进程的独立性,
有在一定程度上节省了空间,
这就是所谓的写时拷贝。
进程终止
进程退出时会销毁PCB以及进程地址空间,
释放掉页表以及其中的各种映射关系,
代码段与数据段所占用的空间也要被还给系统。
当然,进程终止还要将进程执行情况报告给父进程,
如果父进程迟迟不接收,
子进程可能就处于一个僵尸状态(Zombie),
而进程是怎么返回执行情况的呢,
下面就介绍一下进程退出码。
进程退出码
我们平时写main函数的时候经常写return 0;
但是这个return的0有什么意义呢?可以return别的数吗?
这就不得不介绍一下进程退出码的概念了。
当一个进程退出的时候,
可能是因为正常代码执行结束退出了,
也可能是因为发生了一些错误导致进程退出了,
作为管理进程的操作系统,
他需要知道进程是为什么结束的,
就像老板给员工下发了任务,
员工最终是要像老板汇报任务的完成状况的,
是成功完成了,还是遇到了技术难题…
而进程退出码就是充当这个结束信息的。
我们写main函数时通常写return 0,但实际上也能随便return 其他数字,
在linux下可以通过指令echo $?
查看最近一个进程的退出码:
只不过我们不关心它,所以可以随便写。
但是退出码只是一个数字,并不能直观地反映出问题所在,
所以一般而言退出码都有对应的文字描述,
如果是我们自己写的程序,
我们可以自己规定对应信息,
比如0对应程序正常执行结束且执行结果正确,
1对应程序正常执行结束但执行结果不正确。
当然对于系统提供的一套程序,
都有系统自己的一套映射关系,
我们可以通过C语言函数strerror()
查看某一进程退出码对应的信息:
NAMEstrerror - return string describing error numberSYNOPSIS#include <string.h>char *strerror(int errnum);RETURN VALUEThe 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,
是因为进程退出码就提供了这么多:
下面简单测试一下:
echo $?
会打印最近一个进程的退出码,
显然最近一个进程是ls,它的退出码是2,
2对应的错误信息看上面的图就是No such file or dierctory,
而ls的报错信息正好也是这个,所以这就完全对应上了。
不过需要注意,
只有当进程是因为正常运行结束,无论执行结果错误与否,
这时的返回值才是有意义的。
当进程运行时崩溃了的话,
此时的返回码是无意义的:
此时进程一般是收到了退出信号,在下面的进程等待部分会看一下。
进程退出的方式
进程正常的退出方式有三种,
main函数执行结束正常退出,
调用 函数exit() 退出,
调用 系统调用接口_exit() 退出。
当然,进程也有不正常的退出方式,
什么调用abort啦、收到退出信号啦等等,
不过这里不讲。
main函数正常执行结束进程退出没什么好说的,
不过需要注意一下,
通过main函数return+数字返回进程退出码的方式,
其实和exit(退出码)的退出方式其实是一样的,
因为调用main的运行时函数会将main的返回值当做 exit的参数。
所以下面着重介绍对比一下通过调用 exit() 和 _exit() 的退出方式。
exit()和_exit()
NAMEexit - cause normal process termination//exit - 导致正常进程终止SYNOPSIS#include <stdlib.h>void exit(int status);DESCRIPTIONThe 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 processSYNOPSIS#include <unistd.h>void _exit(int status);DESCRIPTIONThe 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;
}
这是第一段代码的运行结果:
这是第二段代码的运行结果:
可以发现第二段代码竟然没有打印。
这里多提一个I/O缓冲的概念:
I/O缓冲是指在内存里开辟一块区域里存放的数据是用来接收用户输入和用于计算机输出的数据以减小系统开销和提高外设效率。
所以printf要打印的数据是存在缓冲区里的,
想要输出得刷新缓冲区。
知道了这一点,我们就可以确认,
exit退出进程时会刷新缓冲区,
_exit退出进程时不会刷新缓冲区。
其实这也侧面说明了一点,
这里C语言的的I/O缓冲区是用户级别的。
exit()是C语言函数,是经过封装的用户操作接口,
可以对用户层面的数据进行操作;
而_exit()是系统调用接口,
系统调用接口向下是操作系统,向上是用户,
所以_exit()无法对用户层面的数据进行操作。
总结一下,exit()实际上最后也会调用_exit()退出进程,
但在这之前还封装了一些功能:
执行用户通过atexit或on_exit定义的清理函数
关闭所有打开的流,所有的缓存数据均被写入
调用_exit
而_exit()就很单纯了,就是简单的退出进程。
进程等待
在进程状态一文中有提到存在着一种特殊的进程状态 —— 僵尸状态(zombie)。
僵尸状态是指子进程已经运行结束准备返回运行结果,
通俗来讲就是子进程已经完成任务等待父进程的回收。
内核维护关于僵尸进程的最小信息集(PID、终止状态、资源使用信息),
以便允许父进程稍后执行等待以获取关于子进程的信息。
只要僵尸进程没有通过等待从系统中删除,
它就会占用内核进程表中的一个槽,
如果这个表已满,就不可能再创建其他进程。
如果父进程终止,那么它的“僵尸”子进程(如果有)将被init(8)采用,
init(8)将自动执行等待以删除僵尸进程,不过这都是后话了。
那么父进程如何接收子进程的执行信息好让操作系统回收子进程呢?
这就是进程等待要解决的问题。
进程等待方法 – wait()和waitpid()
NAMEwait, waitpid - wait for process to change stateSYNOPSIS#include <sys/types.h>#include <sys/wait.h>pid_t wait(int *status);pid_t waitpid(pid_t pid, int *status, int options);DESCRIPTIONBoth 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 VALUEwait(): 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){//childexit(100); }int status = 0;wait(&status);printf("status : %d\n", status);return 0;
}
子进程上来就退出,然后父进程调用wait()等待子进程结束。
运行结果如下:
但是打印出来的status并不是子进程设置的退出码100,
这是因为status有自己的位图结构:
虽然他是一个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){//childexit(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;
}
运行结果如图:
也可以让子进程执行死循环,然后用 kill -9 终止子进程看一下退出信号:
int main()
{pid_t id = fork();assert(id != -1);if (id == 0){//childwhile(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;
运行结果:
像父进程如果调用 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){//childint 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;
}
运行结果如下:
这就是阻塞等待。
相对应的就是非阻塞等待。
非阻塞等待顾名思义,
父进程通过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 10typedef 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;
}
运行结果如下:
发现子进程确实去调用ls命令了,
而且子进程的打印语句也没有执行。
这到底是怎么一回事呢?
进程替换的原理
上面调用exec函数使子进程执行了另一个程序,
这种执行方式叫做进程的程序替换。
当进程调用一种exec函数时,
该进程的用户空间代码和数据完全被新程序替换,
从新程序的启动例程开始执行。
这是调用**fork()**创建完子进程时的样子:
这是调用exec替换后的样子:
待替换的程序会从硬盘加载到内存,
新程序的代码和数据会覆盖子进程的代码和数据,
本来子进程和父进程是共享一份代码和数据的,
但因为进程之间的独立性,这时会进行写时拷贝,
重新开辟一块空间将新程序的代码和数据写入,
然后做一系列进程启动工作,
从而完成进程替代。
紧接着,原来的子进程就变成新进程运行了。
不过,调用exec并不创建新进程,
所以调用exec前后该进程的id并未改变。
进程替换函数
C语言一共提供了六个进程替换函数,
它们都是以exec开头,所以统称exec函数,
并且它们的用法十分接近,
下面对它们进行简单的讲解。
NAMEexecl, execlp, execle, execv, execvp, execvpe - execute a fileSYNOPSIS#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 VALUEThe 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。
这其实是个可变参数列表,
就和我们经常用的printf和scanf一样,
传的参数数量是可以不固定的。
可变参数列表要传的参数其实就是程序的调用方式。
什么是调用方式呢?
我们在命令行调用指令的时候会加各种选项,
这其实就是我们调用指令的方式。
比如我要调用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[])
它们的关系如图所示:
进程替换的使用
进程替换是用可执行程序来替换的,
这个可执行程序可以是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);
相关文章:

【Linux】进程控制
文章目录进程创建简单认识一下fork()函数为什么fork()会有两个返回值fork通过写时拷贝的方式创建子进程进程终止进程退出码进程退出的方式exit()和_exit()进程等待进程等待方法 -- wait()和waitpid()status参数解释waitpid()的pid参数waitpid()的options参数 - 阻塞和非阻塞进程…...

谷歌seo快排技术怎么做?Google排名霸屏推广原理
本文主要分享关于谷歌快速排名的方法和所需要的条件。 本文由光算创作,有可能会被剽窃和修改,我们佛系对待这种行为吧。 首先提出一个问题:谷歌seo快排技术怎么做?如何达到谷歌霸屏的效果? 答案是:利用谷…...

MySQL的优化
目录 一.概念 二.查看SQL执行频率 三.定位低效率执行SQL 定位低效率执行SQL—慢查询日志 操作 定位低效率执行SQL—show processlist 四.explain分析执行计划 字段说明 explain中的id explain中的select_type explain中的type explain中的table explain中的rows ex…...

实现qq群消息接收和发送功能
QQWebsocketClient是什么 实现qq群消息接收和发送功能,基于websocket技术和cqhttp服务开发 一、 效果截图 二、实现思路 使用cqhttp进行socket反向代理,获取qq聊天的所有消息 编写java客户端,连接至cqhttp服务器获取聊天消息 获取聊天消…...

压缩20M文件从30秒到1秒的优化过程
压缩20M文件从30秒到1秒的优化过程 有一个需求需要将前端传过来的10张照片,然后后端进行处理以后压缩成一个压缩包通过网络流传输出去。之前没有接触过用Java压缩文件的,所以就直接上网找了一个例子改了一下用了,改完以后也能使用࿰…...

如何选择合适的固态继电器?
如何选择合适的固态继电器? 在选择固态继电器(SSR)时,应根据实际应用条件和SSR性能参数,特别要考虑到使用中的过流和过压条件以及SSR的负载能力,这有助于实现固态继电器的长寿命和高可靠性。然后࿰…...
SAP 忘记SAP系统Client 000的所有账号密码
忘记SAP系统Client 000的所有账号密码。 Solution 在SAP系统DB中删除账号SAP*,SAP系统会自动创建SAP*这个账号,然后初始密码是“PASS”,这样就获得Client 000 SAP*账号。 Step by Step 以Oracle数据库为例: 1.以<SID>ADM账…...
Connext DDS可扩展类型Extensible Types指南
RTI Connext DDS 可扩展类型Extensible Types指南 可扩展类型Extensible TypesConnextDDSv6.1.1版本,包含了对OMG“DDS的可扩展和动态主题类型Extensible andDynamic Topic Types for DDS”规范1.3版的部分支持,该规范来自对象管理组OMG。这种支持,允许系统以更灵活的方式定义…...
Docker简单使用
文章目录1、安装配置2、服务启动3、Docker镜像下载4、Docker启动容器5、容器的常用命令6、Docker进入容器内部7、宿主机与容器交换文件8、查看日志官网地址:1、安装配置 sudo yum install -y yum-utils 设置镜像地址 sudo yum-config-manager \--add-repo \https:…...

A Time Series is Worth 64 Words(PatchTST模型)论文解读
摘要 我们提出了一种高效的基于Transformer设计的模型,用于多变量时间序列预测和自我监督表征学习(self-supervised learning)。它基于两个关键部分:1、将时间序列分隔成子序列级别的patches,作为Transformer的输入&a…...

微服务学习:SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式
目录 一、高级篇 二、面试篇 实用篇 day05-Elasticsearch01 安装elasticsearch 1.部署单点es 2.部署kibana 一、高级篇 二、面试篇 实用篇 day05-Elasticsearch01 安装elasticsearch 1.部署单点es 1.1.创建网络 因为我们还需要部署kibana容器,因此需要…...

nginx平滑升级
1.平滑升级操作1.1 备份安装目录下的nginxcd /usr/local/nginx/sbin mv nginx nginx.bak1.2 复制objs目录下的nginx到当前sbin目录下cp /opt/software/nginx/nginx-1.20.2/objs/nginx /usr/local/nginx/sbin/1.3 发送信号user2给nginx老版本对应的进程kill -user2 more /usr/lo…...

高可用的“异地多活”架构设计
前言 后台服务可以划分为两类,有状态和无状态。高可用对于无状态的应用来说是比较简单的,无状态的应用,只需要通过 F5 或者任何代理的方式就可以很好的解决。后文描述的主要是针对有状态的服务进行分析。 服务端进行状态维护主要是通过磁盘…...

【面试题】Map和Set
1. Map和Object的区别 形式不同 // Object var obj {key1: hello,key2: 100,key3: {x: 100} } // Map var m new Map([[key1, hello],[key2, 100],[key3, {x: 100}] ])API不同 // Map的API m.set(name, 小明) // 新增 m.delete(key2) // 删除 m.has(key3) // …...

Spring之事务底层源码解析
Spring之事务底层源码解析 1、EnableTransactionManagement工作原理 开启 Spring 事务本质上就是增加了一个 Advisor,当我们使用 EnableTransactionManagement 注解来开启 Spring 事务时,该注解代理的功能就是向 Spring 容器中添加了两个 Bean…...
【华为OD机试真题 Python】创建二叉树
前言:本专栏将持续更新华为OD机试题目,并进行详细的分析与解答,包含完整的代码实现,希望可以帮助到正在努力的你。关于OD机试流程、面经、面试指导等,如有任何疑问,欢迎联系我,wechat:steven_moda;email:nansun0903@163.com;备注:CSDN。 题目描述 请按下列描达构建…...

RuoYi-Vue-Plus搭建(若依)
项目简介 1.RuoYi-Vue-Plus 是重写 RuoYi-Vue 针对 分布式集群 场景全方位升级(不兼容原框架)2.环境安装参考:https://blog.csdn.net/tongxin_tongmeng/article/details/128167926 JDK 11、MySQL 8、Redis 6.X、Maven 3.8.X、Nodejs > 12、Npm 8.X3.IDEA环境配置…...
uboot和linux内核移植流程简述
一、移植uboot流程 1、从半导体芯片厂下载对应的demo,然后编译测试demo版的uboot 开发板基本都是参考半导体厂商的 dmeo 板,而半导体厂商会在他们自己的开发板上移植好 uboot、linux kernel 和 rootfs 等,最终制作好 BSP包提供给用户。我们可…...

【CS224W】(task2)传统图机器学习和特征工程
note 和CS224W课程对应,将图的基本表示写在task1笔记中了;传统图特征工程:将节点、边、图转为d维emb,将emb送入ML模型训练Traditional ML Pipeline Hand-crafted feature ML model Hand-crafted features for graph data Node-l…...

【算法基础】并查集⭐⭐⭐⭐⭐【思路巧,代码短,面试常考】
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中。其特点是看似并不复杂,但数据量…...

C++实现分布式网络通信框架RPC(3)--rpc调用端
目录 一、前言 二、UserServiceRpc_Stub 三、 CallMethod方法的重写 头文件 实现 四、rpc调用端的调用 实现 五、 google::protobuf::RpcController *controller 头文件 实现 六、总结 一、前言 在前边的文章中,我们已经大致实现了rpc服务端的各项功能代…...
【学习笔记】深入理解Java虚拟机学习笔记——第4章 虚拟机性能监控,故障处理工具
第2章 虚拟机性能监控,故障处理工具 4.1 概述 略 4.2 基础故障处理工具 4.2.1 jps:虚拟机进程状况工具 命令:jps [options] [hostid] 功能:本地虚拟机进程显示进程ID(与ps相同),可同时显示主类&#x…...

dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...

使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...

Golang——7、包与接口详解
包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...

nnUNet V2修改网络——暴力替换网络为UNet++
更换前,要用nnUNet V2跑通所用数据集,证明nnUNet V2、数据集、运行环境等没有问题 阅读nnU-Net V2 的 U-Net结构,初步了解要修改的网络,知己知彼,修改起来才能游刃有余。 U-Net存在两个局限,一是网络的最佳深度因应用场景而异,这取决于任务的难度和可用于训练的标注数…...
k8s从入门到放弃之HPA控制器
k8s从入门到放弃之HPA控制器 Kubernetes中的Horizontal Pod Autoscaler (HPA)控制器是一种用于自动扩展部署、副本集或复制控制器中Pod数量的机制。它可以根据观察到的CPU利用率(或其他自定义指标)来调整这些对象的规模,从而帮助应用程序在负…...
虚幻基础:角色旋转
能帮到你的话,就给个赞吧 😘 文章目录 移动组件使用控制器所需旋转:组件 使用 控制器旋转将旋转朝向运动:组件 使用 移动方向旋转 控制器旋转和移动旋转 缺点移动旋转:必须移动才能旋转,不移动不旋转控制器…...
Easy Excel
Easy Excel 一、依赖引入二、基本使用1. 定义实体类(导入/导出共用)2. 写 Excel3. 读 Excel 三、常用注解说明(完整列表)四、进阶:自定义转换器(Converter) 其它自定义转换器没生效 Easy Excel在…...
STL 2迭代器
文章目录 1.迭代器2.输入迭代器3.输出迭代器1.插入迭代器 4.前向迭代器5.双向迭代器6.随机访问迭代器7.不同容器返回的迭代器类型1.输入 / 输出迭代器2.前向迭代器3.双向迭代器4.随机访问迭代器5.特殊迭代器适配器6.为什么 unordered_set 只提供前向迭代器? 1.迭代器…...