在linux中执行命令
在很多时候我们需要在程序中执行命令,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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
可以看到输出并没有我们最后打印的数据,所以也就验证了开始我们说的,exec替换了程序的数据。
[root@localhost build]# ./test
hello word!
[root@localhost build]#
2
3
我们有没有方法进一步验证呢?有的客官,别着急往后看。我们可以写一个新的程序,让这个程序去调用,代码如下:
#被调用程序
#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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
输出:
[root@localhost build]# ./test
current pid:61735
new programe pid:61735
2
3
看吧,在新的程序执行的时候,和调用他的进程进程号是一样的。
不过瘾?我们来点硬核的,我们看一下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;
}
2
3
4
5
6
7
8
9
10
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);
}
2
3
4
5
6
7
我们直接跳到最后来看它是如何替换进程空间的,这里面牵扯的知识太多、我只能做一些简单的介绍,写得不好欢迎指正。
在新的程序执行之前会有许多的前置工作,检查当前的进程、用户线程是否超过限制。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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
然后,调用 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;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
最后调用bprm_execve(bprm, fd, filename, flags);函数来继续执行。
未完待续。。。。。。
