In this tutorial, introduce
Multiple processes: Concurrent vs Parallel. Processor vs CPU vs Core
-
任务分离:系统中的每个进程通常只负责执行一个任务。创建新进程可以让程序将不同的任务分配给不同的进程独立执行,这有助于保持任务之间的独立性,防止相互干扰。比如,一个应用程序可以创建一个新进程来处理文件操作,而主进程负责用户界面操作。
-
资源分配和隔离:每个进程都有自己的地址空间和资源分配。通过创建新进程,操作系统可以更好地隔离资源,防止进程之间发生冲突或资源抢占。
-
进程标识符 (PID):
-
每个进程在操作系统中都有一个唯一的标识符,称为进程标识符 (PID)。当一个新进程被创建时,操作系统会为其分配一个唯一的 PID。这是操作系统在内部识别和管理进程的主要方式。通过 PID,可以区分系统中的每一个进程。
-
-
进程的生命周期:
-
创建阶段:进程是由另一个现有进程(通常是父进程)创建的,使用系统调用(如 Unix/Linux 系统中的
fork()
)。在进程创建时,操作系统会分配资源(如内存、文件描述符等)并分配一个唯一的 PID。 -
运行阶段:进程在被创建后,进入执行阶段,可以进行各种操作,如访问文件、占用 CPU 等。操作系统通过 PID 来管理和调度这些进程。
-
终止阶段:当一个进程完成任务或者遇到某些异常情况(如调用
exit()
或者发生了致命错误),进程将终止。当进程终止时,操作系统会释放该进程所占用的资源,包括内存、打开的文件句柄、网络连接等。然而,进程并不会立刻被完全移除,其状态会变成“僵尸状态”(Zombie),等待父进程确认其已终止。
-
-
父进程和进程终止的报告:
- 进程的终止状态会通过信号(如
SIGCHLD
)报告给父进程。父进程可以通过调用wait()
或waitpid()
来获取已终止子进程的退出状态,并清理该进程的资源。 - 只有在父进程接收到终止状态并进行相应处理后,子进程的 PID 和其他资源才会真正被系统回收。此时,子进程彻底结束,相关资源和 PID 会被释放。
-
具体来说,
wait()
有以下几个功能:-
等待子进程结束:
wait()
会让父进程暂停执行,直到有一个子进程终止。这是“等待”的部分。 -
获取子进程的终止状态:当子进程结束时,
wait()
返回子进程的退出状态(比如正常结束还是异常终止)。这就是“获取”的部分。父进程通过wait()
来得知子进程是如何终止的,以及获取与其终止相关的信息。wait()
的返回值是终止的子进程的 PID,同时还可以通过参数获取子进程的退出状态。这个退出状态能够帮助父进程决定如何处理子进程的结束信息,例如是否需要重新启动这个进程,或者记录其运行结果。-
1.
wait()
的基本使用wait()
的基本作用是让父进程等待任意一个子进程的结束。在wait()
调用时,父进程会暂停执行,直到一个子进程终止为止。当一个子进程结束时,wait()
会返回该子进程的 PID,并且父进程可以获取子进程的退出状态。#include <sys/wait.h> pid_t wait(int *status);
status
:是一个指针,用来存储子进程的退出状态。如果不关心子进程的退出状态,可以传入NULL
。 - 返回值:返回结束的子进程的 PID;如果没有子进程则返回
-1
,并设置errno
。 -
如果子进程在
wait()
之前终止了怎么办?-
僵尸进程(Zombie Process):当子进程终止后,如果父进程还没有调用
wait()
,子进程就会变成一个僵尸进程(Zombie Process)。这个僵尸进程依然占用一些系统资源(如进程表项),直到父进程调用wait()
来收集它的退出状态。 -
如果子进程在
wait()
之前已经终止,父进程在调用wait()
时仍然会正确返回该子进程的退出状态,并回收它的资源。因此,即使子进程提前结束,只要父进程最终调用wait()
,系统资源就会被正确回收,僵尸进程将消失。
-
-
什么时候使用
wait()
?- 当父进程需要等待某个子进程结束时,或者父进程需要知道子进程的退出状态,就会使用
wait()
。 - 常见的使用场景:
- 父进程需要知道子进程是否正常结束。
- 父进程在创建多个子进程后,希望等待它们的终止以继续后续的工作。
- 防止僵尸进程的产生,确保所有子进程资源都被回收。
-
4. 如何处理多个子进程?
如果父进程创建了多个子进程,可以使用以下几种方法来管理这些子进程:
(1)
wait()
循环父进程可以在循环中反复调用
wait()
来等待所有子进程的终止。每调用一次wait()
,就会等待任意一个子进程终止,直到所有子进程都结束。 -
(2)
waitpid()
:处理特定子进程有时,父进程可能不想等待任意子进程结束,而是等待某个特定的子进程。这时可以使用
waitpid()
函数。它允许指定一个特定的子进程 PID,并且提供更多的灵活性,例如是否等待非阻塞地获取子进程状态。#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options);
pid
:指定要等待的子进程的 PID;如果pid == -1
,则表示等待任意一个子进程(类似wait()
)。status
:指针,用于存储子进程的退出状态。options
:可以设置为 0 或WNOHANG
等选项,WNOHANG
表示非阻塞地调用waitpid()
,如果没有子进程结束,函数立即返回而不会阻塞父进程。
-
为什么不能使用
wait(int child_pid)
?如果
wait()
接受的是int
而不是int *
,即wait(int child_pid)
,那意味着: -
传递的是一个值,而不是指针。C 语言是按值传递参数的,因此函数内部无法修改调用者的变量。如果直接传递
int
类型的变量,wait()
无法通过该变量将子进程的退出状态返回给调用者。 -
child_pid
在wait(int child_pid)
中无法传递给wait()
来作为输出值,而wait()
的返回值才是用来获取子进程 PID 的。因此,调用wait(int child_pid)
并没有实际意义,因为wait()
本质上是阻塞等待某个子进程结束并返回其 PID。 -
1. 如何理解
status
的含义?wait()
函数中的status
变量并不是直接给出子进程的退出码(即子进程调用exit()
或返回的值),而是包含了一些关于子进程终止方式的编码信息。为了从status
中提取有用的信息,C 标准库提供了一些宏来解析和处理status
。2. 常用宏函数解析
status
使用这些宏可以从
status
中提取子进程的退出信息:1.
WIFEXITED(status)
:检查子进程是否是正常退出的。- 返回值:如果子进程是通过
exit()
或者返回主函数而终止的,则返回非零值(即true
),否则返回零(false
)。
- 返回值:如果子进程是通过
-
2.
WEXITSTATUS(status)
:获取子进程的退出状态码。- 使用条件:在
WIFEXITED(status)
返回非零时,才能调用WEXITSTATUS(status)
,它返回子进程的退出状态码(即exit()
传递的值或主函数的返回值)。
- 使用条件:在
-
3.
WIFSIGNALED(status)
:检查子进程是否因为信号而被终止。- 返回值:如果子进程是因为接收到未捕获的信号而终止的,则返回非零值(
true
)。
- 返回值:如果子进程是因为接收到未捕获的信号而终止的,则返回非零值(
-
4.
WTERMSIG(status)
:获取导致子进程终止的信号编号。- 使用条件:在
WIFSIGNALED(status)
返回非零时,才能调用WTERMSIG(status)
,它返回终止子进程的信号编号。
- 使用条件:在
-
5.
WIFSTOPPED(status)
:检查子进程是否被暂停。- 返回值:如果子进程因为信号暂停了,则返回非零值。
- 使用条件:在
WIFSTOPPED(status)
返回非零时,才能调用WSTOPSIG(status)
,它返回导致暂停的信号编号。
-
6.
WSTOPSIG(status)
:获取导致子进程暂停的信号编号。- 使用条件:在
WIFSTOPPED(status)
返回非零时,才能调用WSTOPSIG(status)
,它返回导致暂停的信号编号。
- 使用条件:在
- 当父进程需要等待某个子进程结束时,或者父进程需要知道子进程的退出状态,就会使用
-
-
-
fork()
和父子进程的关系:-
fork()
系统调用:在操作系统中,fork()
是一个用于创建新进程的系统调用。它会将当前的进程(称为父进程)复制一份,生成一个新的进程(称为子进程)。这个操作被称为“进程分叉”(forking a process)。 -
子进程的特性:
- 进程克隆:子进程是父进程的精确副本,它继承了父进程的大部分属性,包括代码段、数据段、文件描述符、环境变量等。它和父进程运行相同的程序,并从
fork()
调用点继续执行。 - 唯一的 PID:尽管子进程继承了几乎所有父进程的属性,它有一个关键的区别:子进程拥有自己独立的进程标识符 (PID),这使得操作系统能够区分父进程和子进程。
- 进程克隆:子进程是父进程的精确副本,它继承了父进程的大部分属性,包括代码段、数据段、文件描述符、环境变量等。它和父进程运行相同的程序,并从
-
fork()
的返回值:- 父进程:在父进程中,
fork()
的返回值是子进程的 PID。这样父进程可以通过这个 PID 来管理或与子进程进行交互。 - 子进程:在子进程中,
fork()
的返回值为 0。通过返回值,子进程能够知道自己是一个新创建的进程,而不是继续作为父进程执行。
- 父进程:在父进程中,
-
父子进程的独立性:
- 独立的执行路径:虽然子进程是父进程的克隆,但父子进程是独立的,它们可以在不同的时间、以不同的速率运行,并且可以独立地对系统资源进行操作。举例来说,父进程可能会继续进行其他任务,而子进程可以执行不同的操作,甚至可以通过
exec()
替换其内存中的程序来运行新的可执行文件。 - 资源共享与独立:某些资源,如打开的文件描述符,父子进程可以共享,但它们的内存空间、进程计数器和堆栈是独立的。子进程对自己内存空间的修改不会影响到父进程,反之亦然。
- 独立的执行路径:虽然子进程是父进程的克隆,但父子进程是独立的,它们可以在不同的时间、以不同的速率运行,并且可以独立地对系统资源进行操作。举例来说,父进程可能会继续进行其他任务,而子进程可以执行不同的操作,甚至可以通过
-
典型的使用场景:
- 进程分叉常用于并发执行任务。例如,一个服务器进程可以使用
fork()
创建多个子进程来处理不同的客户端请求,从而提高效率。 fork()
通常与exec()
系列调用结合使用:fork()
负责创建新进程,而exec()
则让新进程执行不同的程序。这种模式通常用于创建子进程来运行不同的任务或执行外部程序。
- 进程分叉常用于并发执行任务。例如,一个服务器进程可以使用
-
-
exec()
系列函数的作用-
执行新程序:当进程调用
exec()
时,当前进程的内存空间(代码段、数据段等)会被新程序的内容替换掉。也就是说,exec()
并不会创建一个新的进程,而是让当前的进程停止执行原有的程序,并开始执行一个新的可执行文件。 -
进程不变:调用
exec()
后,进程的 PID 保持不变,但内存中的程序代码被新程序取代。换句话说,进程还是同一个进程,只是它“改头换面”执行了一个不同的程序。
-
-
exec()
和fork()
的结合使用通常情况下,
exec()
函数与fork()
系统调用一起使用。这是因为fork()
创建了一个新的进程,而exec()
则用于让这个新进程运行一个完全不同的程序。这种模式非常常见,尤其在操作系统中,当父进程希望创建子进程来运行某个特定任务时,会先调用fork()
生成子进程,然后在子进程中调用exec()
来执行新的程序。例如:
fork()
创建新进程:父进程调用fork()
,创建一个新的子进程。exec()
替换程序:子进程调用exec()
,把自己的内存内容替换成新程序,并开始执行该程序。-
语言背景
fork()
和exec()
是在C语言中用于与操作系统交互的系统调用,主要应用于类 Unix 系统(如 Linux、macOS)。这些系统调用是底层的函数,直接与操作系统的内核进行交互。 -
fork()
系统调用的功能fork()
是 Unix-like 系统中用于创建新进程的系统调用。它会将当前进程(称为父进程)复制一份,生成一个新的子进程。新创建的子进程是父进程的几乎精确副本,除了它有自己独立的进程 ID (PID)。fork()
的返回值调用
fork()
后,它会有不同的返回值,具体取决于在哪个进程中: -
返回值为 -1:如果
fork()
返回-1
,表示子进程的创建失败。这可能是由于系统资源不足或者其他错误。此时没有创建任何新进程。 -
返回值为 0:如果
fork()
在子进程中返回0
,这意味着当前的进程是子进程。子进程可以通过这个返回值知道它是由fork()
创建的。 -
返回值为正整数:如果
fork()
在父进程中返回了一个正整数,该正整数就是新创建的子进程的 PID。父进程可以通过这个返回值知道子进程已经成功创建,并使用这个 PID 来管理子进程。 -
返回的 PID 类型
返回的进程 ID 类型是
pid_t
,这个类型是在头文件sys/types.h
中定义的。pid_t
是一种整数类型,通常用来表示进程 ID。在不同的系统中,pid_t
可能是不同的具体类型(例如int
或long
),但它本质上是一个能容纳进程标识符的整型数据。
-
sys/types.h
-
是一个头文件,主要在 Unix-like 操作系统(如 Linux 和 macOS)中使用。它定义了一些常用的数据类型,这些类型用于各种系统调用和标准库函数中。
sys/types.h
的作用sys/types.h
头文件提供了用于表示系统中各种资源标识符的类型定义,比如文件大小、进程 ID、用户 ID 等。这些类型抽象了底层系统实现的具体细节,以便于在不同的平台上保证代码的可移植性。常用的类型定义
以下是
sys/types.h
中常见的一些类型定义:-
pid_t
:用于表示进程标识符 (PID),这是fork()
系统调用返回的类型。pid_t
通常是一个整数类型。- 例如:
pid_t fork(void);
- 例如:
-
uid_t
:用于表示用户 ID(用户标识符)。- 例如:
uid_t getuid(void);
获取当前进程的用户 ID。
- 例如:
-
gid_t
:用于表示组 ID(组标识符)。- 例如:
gid_t getgid(void);
获取当前进程的组 ID。
- 例如:
-
size_t
:用于表示对象或内存块的大小,常用于malloc()
或sizeof()
等函数的返回类型。它通常是一个无符号整数类型。
-
-
-
为什么需要
sys/types.h
-
可移植性:不同平台可能对相同的数据使用不同的底层实现,比如
int
或long
。通过sys/types.h
提供的类型定义,可以保证这些数据类型在不同系统中的一致性和可移植性。例如,进程 ID 可能在一个系统中是int
,而在另一个系统中是long
,而使用pid_t
就可以屏蔽这些底层差异。 -
抽象系统资源:头文件提供了与操作系统资源(如进程、文件、用户等)相关的抽象数据类型,帮助程序员使用这些资源,而不必担心底层实现细节。
-
-
一个简单的例子
-
off_t
:用于表示文件的偏移量。它通常用于文件读写中的位置偏移。- 例如:
off_t lseek(int fd, off_t offset, int whence);
- 例如:
-
ssize_t
:类似于size_t
,但它是带符号的,通常用于返回可能为负值的字节数。- 例如:
ssize_t read(int fd, void *buf, size_t count);
- 例如:
-
- 进程的终止状态会通过信号(如
fork()
的工作原理
-
创建两个独立的进程: 当
fork()
成功调用时,Unix 操作系统会创建一个新的进程(子进程),父进程和子进程都会拥有独立的地址空间。这意味着每个进程都会有自己的内存副本,并且父进程对其地址空间的任何修改都不会影响子进程,反之亦然。 -
复制地址空间: 虽然
fork()
会创建一个子进程,表面上看似乎是直接复制了父进程的整个内存空间(代码段、数据段、堆栈等),但大多数现代 Unix-like 操作系统采用了一种称为写时拷贝(Copy-On-Write, COW)的技术。写时拷贝意味着父子进程在fork()
后会共享同一个内存副本,但只有在其中一个进程试图修改这块内存时,操作系统才会为其分配新的内存。这样可以显著提高效率,避免在fork()
调用时立即复制整个地址空间。 -
父子进程的执行: 一旦
fork()
成功调用,父进程和子进程的执行从fork()
之后的下一条语句开始。这是fork()
的一个非常独特之处:它会在两个不同的进程中返回,因此父子进程是从同一个位置继续执行代码。- 在父进程中:
fork()
返回的是子进程的 PID(一个正值),因此父进程可以通过返回值知道子进程已经成功创建。 - 在子进程中:
fork()
返回0
,这意味着子进程可以通过这个返回值确认自己是子进程。
- 在父进程中:
#include <stdio.h> // 标准输入输出库
#include <stdlib.h> // 标准库,包含exit()函数
#include <unistd.h> // Unix标准库,包含fork()、sleep()等系统调用
#include <sys/types.h> // 包含pid_t类型定义
#include <string.h> // 字符串操作函数,如strcpy()
int main(int argc, char *argv[]){
// 定义一个字符数组buf,长度为50,初始化为"Original test strings"
char buf[50] = "Original test strings";
pid_t pid; // 定义一个pid_t类型的变量pid,用来存储进程ID
// 打印进程开始分叉的提示信息
printf("Process start to fork\n");
// 调用fork()创建新进程,返回值存入pid变量中
pid = fork();
// 检查fork()是否失败
if(pid == -1){
// 如果fork()返回-1,表示创建子进程失败,打印错误信息并退出
perror("fork");
exit(1);
}
else
{
// 子进程的代码部分
if(pid == 0){ // 如果pid为0,表示这是子进程
// 子进程修改buf中的字符串
strcpy(buf, "Test strings are updated by child.");
// 打印子进程中的提示信息和修改后的buf内容
printf("I'm the Child Process: %s\n", buf);
exit(0); // 子进程正常退出
}
// 父进程的代码部分
else{
// 父进程睡眠3秒,确保子进程先运行
sleep(3);
// 打印父进程中的提示信息和buf内容(父进程的buf没有被子进程修改)
printf("I'm the Parent Process: %s\n", buf);
exit(0); // 父进程正常退出
}
}
return 0;
}
主要概念:
- 进程复制:调用
fork()
后,操作系统会复制父进程,生成子进程,子进程与父进程有独立的地址空间。 - 独立执行:子进程对
buf
的修改不会影响父进程,因为父子进程拥有独立的内存副本。
1. fork()
后的父子进程并发执行:
当 fork()
被调用时,操作系统会创建一个子进程,子进程与父进程并发执行。父进程和子进程实际上是在同一个终端中输出信息,因为它们共享同一个终端作为标准输出设备(stdout
)。因此,不论是父进程还是子进程,它们的 printf()
输出都会显示在同一个终端窗口。
2. 父子进程的执行顺序:
-
并发执行:父进程和子进程几乎同时开始执行,但操作系统的进程调度程序决定哪个进程先执行以及执行多长时间。因为父子进程并不是串行的(也不是严格意义上的同步),因此它们的执行顺序可能不同,每次运行时结果都可能不同。虽然在这个例子中,子进程先执行了
printf()
,但在某些情况下,父进程可能先执行,取决于调度。 -
sleep() 的作用:在您的程序中,父进程调用了
sleep(3)
,这使得父进程会等待 3 秒,让子进程有足够的时间完成它的任务(即修改buf
并打印输出)。这就是为什么子进程的输出会先显示在终端中,而父进程的输出在 3 秒后才出现。
3. 输出在同一个终端:
父进程和子进程的标准输出 (stdout
) 都指向同一个终端,因此不论哪个进程调用 printf()
,输出都会显示在同一个地方。这是因为终端设备是一个共享的资源,多个进程可以通过它进行输出。
4. 子进程运行到何时结束?
子进程会从 fork()
调用的下一行开始执行。在您的代码中:
-
如果是子进程(
pid == 0
),它会运行以下语句:strcpy(buf, "Test strings are updated by child."); printf("I'm the Child Process: %s\n", buf); exit(0); // 子进程在这里退出
-
子进程修改了
buf
的值并打印后,调用exit(0)
正常退出。exit()
终止了子进程的执行,释放其资源,进程生命周期结束。 -
父进程则在
sleep(3)
之后继续执行自己的任务,打印buf
的内容,最终也调用exit(0)
退出。 -
6. 从
fork()
之后的执行到结束的流程:- 子进程:一旦
fork()
成功,子进程从fork()
返回值0
处开始执行,运行自己的代码,最终调用exit(0)
结束。 - 父进程:父进程从
fork()
返回子进程的 PID 处继续执行,运行sleep(3)
,然后继续执行后面的代码,最后调用exit(0)
结束。
- 子进程:一旦
Linux 进程树(A Tree of Processes in Linux)
进程树的内容和作用:
-
init
进程 (PID = 1):init
是 Linux 系统中第一个启动的进程,它是所有其他用户进程和内核线程的祖先进程,位于进程树的根节点。init
进程的主要任务是初始化系统并启动其他进程,比如login
、kthreadd
等。
-
父子关系:
- 每个进程都有父进程和可能的子进程。通过图中的连线可以看到父进程创建了子进程。比如:
login
(PID = 8415) 是init
的子进程。bash
(PID = 8416) 是login
的子进程。ps
(PID = 9298) 和emacs
(PID = 9204) 是bash
的子进程。
- 每个进程都有父进程和可能的子进程。通过图中的连线可以看到父进程创建了子进程。比如:
-
内核线程:
kthreadd
(PID = 2)是 Linux 内核创建的线程管理进程,专门用于创建和管理其他内核线程,如khelper
(PID = 6) 和pdflush
(PID = 200)。
-
sshd
进程:sshd
(PID = 3028 和 PID = 3610)是用于管理远程登录的 Secure Shell Daemon 进程,允许用户通过 SSH 连接到系统。- 子进程
tcsh
(PID = 4005)是由sshd
创建的 shell 进程,用于处理远程登录的用户命令。
#include <stdio.h> // 标准输入输出函数库
#include <stdlib.h> // 包含exit()函数
#include <unistd.h> // 包含fork()和execve()函数
#include <sys/types.h> // 定义pid_t类型
#include <string.h> // 包含字符串操作函数
int main(int argc, char *argv[]){
pid_t pid; // 用来存储进程ID
pid = fork(); // 创建子进程
if (pid < 0) {
printf("Fork error!\n");
exit(1);
}
else {
// 子进程
if (pid == 0) {
printf("This is child process.\n");
printf("Child process id is %d\n", getpid()); // 打印子进程的PID
// 使用 execve() 替换子进程的地址空间
char *arg[] = {argv[1], NULL}; // 准备传递给新程序的参数(argv[1] 是新程序的路径)
execve(argv[1], arg, NULL); // 使用 execve() 执行新程序
printf("Error: execve failed to execute test program!\n"); // 如果 execve() 失败,则执行该行
}
// 父进程
else {
printf("This is farther process.\n");
printf("Farther process id is %d\n", getpid()); // 打印父进程的PID
}
}
return 0;
}
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("Test process id is %d\n", getpid()); // 打印当前进程的PID
printf("Test completed!\n");
return 0;
}
gcc -o execve execve.c # 编译父子进程的代码
gcc -o test test.c # 编译测试程序
./execve ./test # 运行父进程,并让子进程执行“test”程序
详细解析和中文注释:
-
创建子进程:
fork()
函数被调用,创建一个新的子进程。如果fork()
返回0
,表示这是子进程;如果返回正值,则表示这是父进程。- 如果
fork()
返回负值,则表示创建子进程失败。
-
子进程执行
execve()
:- 子进程执行
execve(argv[1], arg, NULL)
,这会使用新的可执行文件(即argv[1]
,在这个例子中为./test
)替换当前子进程的内存空间。 execve()
:这是一个系统调用,用于将当前进程的地址空间替换为新程序的地址空间。换句话说,子进程不再执行原来的代码,而是执行新的程序文件test.c
。- 替换后的子进程继续运行新程序
test.c
中的代码,打印Test process id is ...
和Test completed!
。
- 子进程执行
-
父进程执行:
- 父进程不受
execve()
的影响,它继续执行自己的代码。它在子进程之后打印出This is farther process.
和父进程的 PID。
- 父进程不受
-
终端输出解释:
- 当父进程和子进程执行后,结果显示在同一个终端:
- 子进程输出:子进程先输出
This is child process.
和它的 PID,随后被execve()
替换为test.c
程序,并输出Test process id is ...
和Test completed!
。 - 父进程输出:父进程输出
This is farther process.
,并打印出父进程的 PID。
- 子进程输出:子进程先输出
- 当父进程和子进程执行后,结果显示在同一个终端:
关键点:
操作系统如何处理命令行参数以及 execve()
调用中的参数传递。让我们详细解释:
execve()
中的参数和程序的命令行
1. argv[1]
和 argv[0]
的区别
- 在 C 程序中,
argv[]
是命令行参数的数组。通常:argv[0]
是正在运行的程序的名称或路径。argv[1]
及后续元素是传递给程序的其他命令行参数。./execve ./test