在很多时候我们需要在程序中执行命令,linux提供的方法也很多、但是不够灵活,接下来介绍几种linux执行命令的方法。
exec
当一个程序开始调用一种exec函数时(为什么要强调一种?因为linux中大致提供了7种exec函数),该进程执行的程序完全替换为新程序,从其main函数开始执行,所以前后的进程ID并未改变,exec只是在磁盘上用新程序替换了当前程序的正文段、数据段、堆、栈。这样就意味着写在exec之后的代码都不会被执行。
具体可以看man page对这个系列函数的解释:
==On a successful function call, it does not return anything as the current process gets entirely replaced with the process specified in the function. So, any lines that are written after the `execl()` function would not get executed.==
这七个函数分别是exec、execl、execv、execle、execve、execlp、execvp、fexecve。这些函数的第一个区别是前四个函数使用路径名作为参数,后两个取文件名作为参数,最后一个指定文件描述符作为参数。如果文件名包含'/',也会视为路径名。可变参数列表的第一个参数是程序名称。
注意:
execl、execle、execlp三个函数签名都是(const char *__file, const char *__arg, ...)
这种格式。这种语法强制说明了最后一个参数必须是一个指针,否则会被解释成整型,如果一个整型数与char*
的长度不同,那么exec函数执行的实际参数将会出错。
我们写一个简单程序来具体演示一下:
#include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> int main() { char *command = "echo \"hello word!\""; int status; status = execl("/bin/sh", "sh", "-c", command, (void *)0); if (status == -1) { printf("error exec %s\n", strerror(errno)); return -1; } printf("after execl\n"); return 0; }
可以看到输出并没有我们最后打印的数据,所以也就验证了开始我们说的,exec替换了程序的数据。
[root@localhost build]# ./test hello word! [root@localhost build]#
我们有没有方法进一步验证呢?有的客官,别着急往后看。我们可以写一个新的程序,让这个程序去调用,代码如下:
#被调用程序 #include <stdio.h> #include <unistd.h> int main() { printf("new programe pid:%d\n", getpid()); return 0; } #调用程序 #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> int main() { char *command = "/bin/print_pid"; int status; printf("current pid:%d\n", getpid()); status = execl("/bin/sh", "sh", "-c", command, (void *)0); if (status == -1) { printf("error exec %s\n", strerror(errno)); return -1; } printf("after execl\n"); return 0; }
输出:
[root@localhost build]# ./test current pid:61735 new programe pid:61735
看吧,在新的程序执行的时候,和调用他的进程进程号是一样的。
不过瘾?我们来点硬核的,我们看一下linux内核是如何写的,前面的具体细节调用我就不说了,execl最后是去调用execve
int execve(const char *filename, char *const argv[], char *const envp[]) { int ret = sys_execve(filename, argv, envp); if (ret < 0) { SET_ERRNO(-ret); ret = -1; } return ret; }
execve又去调用syscall
,这个函数具体细节就不说了,感兴趣的可以自己去看,因为他里面是内联的汇编代码调用系统调用,展开来说要开始介绍汇编了,而且每个架构的寄存器、中断号还不一样。
static __attribute__((unused)) int sys_execve(const char *filename, char *const argv[], char *const envp[]) { return my_syscall3(__NR_execve, filename, argv, envp); }
我们直接跳到最后来看它是如何替换进程空间的,这里面牵扯的知识太多、我只能做一些简单的介绍,写得不好欢迎指正。
在新的程序执行之前会有许多的前置工作,检查当前的进程、用户线程是否超过限制。binprm结构用于保存加载二进制文件时使用的参数。例如,它包含vm_area_struct,表示将在给定地址空间中连续间隔内的单个内存区域,将在该空间中加载应用程序。mm字段,它是二进制文件的内存描述符,指向内存顶部的指针以及许多其他不同的字段。
if ((current->flags & PF_NPROC_EXCEEDED) && is_rlimit_overlimit(current_ucounts(), UCOUNT_RLIMIT_NPROC, rlimit(RLIMIT_NPROC))) { retval = -EAGAIN; goto out_ret; } current->flags &= ~PF_NPROC_EXCEEDED; bprm = alloc_bprm(fd, filename); if (IS_ERR(bprm)) { retval = PTR_ERR(bprm); goto out_ret; } retval = count(argv, MAX_ARG_STRINGS); if (retval == 0) pr_warn_once( "process '%s' launched '%s' with NULL argv: empty string added\n", current->comm, bprm->filename);
然后,调用 bprm_stack_limits(bprm)
来处理新程序的栈限制。这可能包括调整栈大小或其他栈相关的设置。如果在这个过程中发生错误(retval < 0
),则会跳转到 out_free
标签,进行资源释放。包括将用户态的参数拷贝到内核空间:
retval = bprm_stack_limits(bprm); if (retval < 0) goto out_free; retval = copy_string_kernel(bprm->filename, bprm); if (retval < 0) goto out_free; bprm->exec = bprm->p; retval = copy_strings(bprm->envc, envp, bprm); if (retval < 0) goto out_free; retval = copy_strings(bprm->argc, argv, bprm); if (retval < 0) goto out_free;
最后调用bprm_execve(bprm, fd, filename, flags);
函数来继续执行。
未完待续。。。。。。