在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include
pid_t fork(void);
这个函数不接受任何参数,当调用这个函数时,它会创建一个新的进程。新的进程(称为子进程)是当前进程(称为父进程)的一个副本。子进程和父进程在执行 fork 之后会有各自独立的运行环境。
fork 函数的返回值是进程 ID(PID)。在父进程中,fork 返回子进程的 PID;在子进程中,fork 返回 0
。如果创建新进程失败
,fork 会返回 -1
。
当进程调用 fork
函数,控制转移到内核中的 fork
代码后,内核会执行以下步骤:
复制进程地址空间:内核会创建一个新的进程地址空间,并将父进程的地址空间复制到新的地址空间中。这包括代码、堆栈、数据等。
复制进程上下文:内核会复制父进程的进程上下文,包括寄存器值、程序计数器、环境变量、打开的文件描述符等。
设置返回值:在父进程中,fork
函数返回新创建的子进程的 PID;在子进程中,fork
函数返回 0。
将新进程添加到进程调度队列:内核会将新创建的子进程添加到进程调度队列,等待被调度执行。
请注意,虽然子进程是父进程的副本,但它们有各自独立的运行环境。例如,它们有各自的地址空间,所以在一个进程中的修改不会影响到另一个进程。此外,它们的 PID 也是不同的。
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以
开始它们自己的旅程,看如下程序。
#include
#include
#include
#include
int main()
{
pid_t pid;
printf("Before: pid is %d\n",getpid());
if((pid = fork())==-1)//创建失败返回-1
{
perror("fork()");
exit(1);
}
printf("After: pid is %d,fork return %d\n",getpid(),pid);
sleep(1);
return 0;
}
这里看到了三行输出,一行before,两行after。进程17860先打印before消息,然后它有打印after。另一个after消息由17861打印的。注意到进程17861没有打印before,为什么呢?如下图所示
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定
。
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
系统中有太多的进程
实际用户的进程数超过了限制
进程退出场景
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止
正常终止(可以通过 echo $? 查看进程退出码):
_exit
是一个在 Unix-like 系统中用于结束当前进程的函数。这个函数的原型如下:
#include
void _exit(int status);
这个函数接受一个整数参数 status
,这个参数是进程的退出状态。当调用这个函数时,当前进程会立即结束,并返回 status
给操作系统。操作系统通常会将这个状态值提供给父进程。
与 exit
函数相比,_exit
函数不会调用任何注册的退出处理函数(atexit
或 on_exit
注册的函数),也不会刷新标准 I/O 缓冲区。它直接结束进程并关闭所有打开的文件描述符。
以下是一个 _exit
的使用示例:
#include
#include
int main() {
printf("Hello, World!");
_exit(-1); // 结束进程并返回 0
}
不会刷新缓冲区,所以hello world不会打印在stdout。
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。
exit
是一个在 C 和 C++ 中常用的函数,用于结束当前进程并返回一个状态值给操作系统。这个函数的原型如下:
#include
void exit(int status);
这个函数接受一个整数参数 status
,这个参数是进程的退出状态。当调用这个函数时,当前进程会立即结束,并返回 status
给操作系统。操作系统通常会将这个状态值提供给父进程或 shell。
以下是一个 exit
的使用示例:
#include
#include
int main() {
printf("Hello, World!\n");
exit(0); // 结束进程并返回 0
}
在这个例子中,我们首先打印一条消息,然后调用 exit(0);
来结束进程。0
是进程的退出状态,通常表示进程成功结束。如果你在 shell 中运行这个程序,你可以使用 echo $?
命令来查看最后一个进程的退出状态。
exit最后也会调用_exit, 但在调用exit之前,还做了其他工作:
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程
。最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
#include
#include
pid_t wait(int*status);
返回值:
成功则返回被等待进程pid,失败则返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
wait()函数是POSIX标准定义的系统调用,它的作用主要是让父进程等待子进程结束,并且可以获得子进程结束时的退出状态。这个函数对于进程控制和资源回收非常重要。
这里的wait()函数等待任何一个子进程结束,如果有任何子进程结束,就会立即返回
。如果没有子进程结束,父进程将被阻塞,直到至少有一个子进程结束
。如果调用wait()时没有任何子进程存在,函数会立即返回-1,并将errno设置为ECHILD。
函数参数解释:
int *status: 这是一个指向整数的指针,用于存储子进程的退出状态。子进程的退出状态可以包含很多信息,如子进程是正常退出还是因为信号被终止,正常退出的返回值是多少,或者是哪个信号导致子进程终止。如果你对这个信息不感兴趣,可以传入NULL。
函数返回值解释:
#include
#include
#include
#include
#include
#include
#include
int main()
{
pid_t pid;
if((pid = fork())==-1)
{
perror("fork");
exit(1);
}
if ( pid == 0 )//子进程
{
sleep(20);
exit(10);
}
else //父进程
{
int status;
int ret = wait(&status);
if(ret==-1)
{
perror("wait failed");
exit(EXIT_FAILURE);
}
if(WEXITSTATUS(status))
{
printf("Child process exited with ret:%d\n",WEXITSTATUS(status));
}
}
return 0;
}
waitpid
函数是一个比 wait
更为灵活的进程等待函数。它可以等待指定的进程结束,并且可以设置不同的选项来控制 waitpid
的行为。`
函数原型如下:
#include
#include
pid_t waitpid(pid_t pid, int *status, int options);
函数参数说明:
pid
: 等待的目标子进程的进程标识符(PID)。
pid
> 0,waitpid
等待其进程ID与 pid
相等的子进程。pid
== -1,waitpid
的行为与 wait
函数相同,即等待任何子进程。pid
== 0,waitpid
等待其组ID等于调用进程组ID的任何子进程。pid
< -1,waitpid
等待其组ID等于 pid
绝对值的任何子进程。status
: 与 wait
函数中的 status
参数相同,用来存储子进程的退出状态
。如果不关心退出状态,可以传递 NULL
。options
: 提供额外的选项来控制 waitpid
的行为。
WNOHANG
: 表示非阻塞等待。如果没有已经结束的子进程,waitpid
会立即返回0,而不是阻塞等待。WUNTRACED
: 除了返回终止子进程的信息外,还返回因信号而停止的子进程信息。WCONTINUED
: 返回因收到 SIGCONT
信号而恢复执行的已停止子进程的状态信息。函数返回值说明:
WNOHANG
选项且没有子进程退出,返回0。pid
参数。一个基本的 waitpid
使用例子:
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if(pid<0)//创建子进程失败
{
perror("fork failed");
exit(EXIT_FAILURE);
//exit 函数是标准库函数,定义在 头文件中。
//EXIT_SUCCESS 和 EXIT_FAILURE 是在 头文件中定义的两个宏。
//它们用来表示标准的成功和失败退出码:
//EXIT_SUCCESS 通常被定义为 0,用于表示程序成功地执行了所需操作。
//EXIT_FAILURE 通常被定义为 1(尽管在某些系统中可能有不同的值),
//用于表示程序由于某种错误而没有成功执行所需操作。
}
else if(pid == 0)
{
// 子进程
printf("This is a child process with pid is:%d\n",getpid());
exit(42);//子进程退出,42做为退出码。
}
else
{
// 父进程
int status;
pid_t wpid;
// 使用WNOHANG,这样即使子进程未结束,waitpid也会立即返回
do
{
wpid = waitpid(pid,&status,WNOHANG);
if(wpid==0)
{
printf("No child process exit,please wait\n");
sleep(1);
}
} while (wpid==0);
if (wpid == -1)
{
perror("wait failed");
exit(EXIT_FAILURE);
}
if(WIFEXITED(status))
{
printf("Child process exited with status:%d\n",WIFEXITED(status));
}
}
return 0;
}
在这个例子中,我们使用了 WNOHANG
选项,这样父进程就可以在子进程还在运行时执行其他任务,而不是一直阻塞等待。我们通过循环和 waitpid
函数来不断检查子进程状态,直到其退出。如果子进程已经退出,我们会打印其返回状态。
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
下面是 status
的位图表示(只考虑低 16 位):
高------>低
15 14 | 13 12 11 10 9 8 | 7 6 5 4 3 2 1 0
exit
、_exit
或 _Exit
函数结束的,那么这 8 位包含了函数的参数值,也就是子进程的退出状态。如果子进程是被信号终止的(被信号所杀),那么这 8 位的值是 0。你可以使用一些宏来检查和获取 status
中的信息,这些宏在
头文件中定义。例如,WIFEXITED(status)
可以检查子进程是否正常结束,WEXITSTATUS(status)
可以获取子进程的退出状态,WIFSIGNALED(status)
可以检查子进程是否被信号终止,WTERMSIG(status)
可以获取终止子进程的信号编号。代码例子如下:
#include
#include
#include
#include
#include
#include
#include
int main()
{
pid_t pid;
if ( (pid=fork()) == -1 )
perror("fork"),exit(1);
if ( pid == 0 )
{
sleep(20);
exit(10);
}
else
{
int st;
int ret = wait(&st);
if ( ret > 0 && ( st & 0X7F ) == 0 )
{ // 正常退出
printf("child exit code:%d\n", (st>>8)&0XFF);
}
else if( ret > 0 )
{ // 异常退出
printf("sig code : %d\n", st&0X7F );
}
}
}
#include // 引入waitpid()函数
#include // 引入printf()函数
#include // 引入exit()函数
#include // 引入字符串操作函数
#include // 引入错误处理
#include // 引入fork()和getpid()函数
#include // 引入pid_t类型
int main()
{
pid_t pid; // 声明一个变量来存储进程ID
pid = fork(); // 创建一个子进程
if(pid < 0) // 如果fork()返回负值,表示出现错误
{
printf("%s fork error\n",__FUNCTION__); // 打印错误信息
return 1; // 返回1表示出现错误
}
else if( pid == 0 ) // 如果fork()返回值为0,表示这是子进程
{
printf("child is run, pid is : %d\n",getpid()); // 打印子进程的PID
sleep(5); // 子进程暂停5秒
exit(257); // 子进程退出,返回257
}
else // 如果fork()返回正值,表示这是父进程
{
int status = 0;
pid_t ret = waitpid(-1, &status, 0); // 父进程等待子进程结束
printf("this is test for wait\n");
if( WIFEXITED(status) && ret == pid ) // 如果子进程正常结束
{
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status)); // 打印子进程的返回值
}
else // 如果子进程未正常结束
{
printf("wait child failed, return.\n"); // 打印错误信息
return 1; // 返回1表示出现错误
}
}
return 0; // 主函数返回0表示程序正常结束
}
#include
#include
#include
#include
#include
int main()
{
pid_t pid;
pid = fork();
if(pid < 0)
{
printf("%s fork error\n",__FUNCTION__);
return 1;
}
else if( pid == 0 )
{ //child
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(1);
}
else
{
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
if( ret == 0 )
{
printf("child is running\n");
}
sleep(1);
}while(ret == 0);
if( WIFEXITED(status) && ret == pid )
{
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
}
else
{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
进程程序替换的原理主要涉及到两个系统调用:fork()
和 exec()
。
fork():这个系统调用会创建一个新的进程,这个新进程是当前进程的一个副本。新进程会继承父进程的代码、数据、堆栈、环境变量、打开的文件描述符等。新进程和父进程的主要区别在于它们有不同的进程ID和不同的父进程ID。
exec():这个系统调用会替换当前进程的代码和数据段,然后开始执行新程序的main()函数。这个过程被称为进程替换,因为新程序取代了当前进程的代码和数据段。进程的堆栈、环境变量和打开的文件描述符等其他属性不会改变。
这两个系统调用通常一起使用,以在一个新的进程中执行一个新的程序。例如,当你在shell中运行一个命令时,shell会首先调用fork()创建一个新的进程,然后在新进程中调用exec()来执行你指定的命令。
这就是进程程序替换的基本原理。如果你有其他问题,欢迎随时向我提问。
其实有六种以exec开头的函数,统称exec函数:
#include `
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 execve(const char *path, char *const argv[], char *const envp[]);
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
execl()
是一个在 Unix 和 Linux 系统中用于执行新程序的函数。它是 exec
函数族的一部分,这些函数允许一个进程加载和执行一个新的程序,替换当前进程的映像。execl()
函数的原型如下:int execl(const char *path, const char *arg, ...);
参数说明:
path
:这是要执行的新程序的路径。arg
:这是传递给新程序的参数列表。参数列表以 NULL
结束。execl()
函数需要一个以 NULL
结尾的参数列表。第一个参数通常是程序自身的名称。execl()
来执行一个位于 /bin/ls
的程序,你可以这样做:execl("/bin/ls", "ls", "-l", NULL);
这将会执行 ls
程序,并传递 -l
作为参数。NULL
表示参数列表的结束。
如果 execl()
函数成功,它将不会返回。如果函数返回,那么意味着有错误发生,你可以检查 errno
来查看错误代码。
#include
#include
int main()
{
execl("/bin/ls", "ls", "-l", NULL);
return 0;
}
execlp调用举例如下:
execlp()
是一个在 Unix 和 Linux 系统中用于执行新程序的函数。它是 exec
函数族的一部分,这些函数允许一个进程加载和执行一个新的程序,替换当前进程的映像。
execlp()
函数的原型如下:
int execlp(const char *file, const char *arg, ...);
参数说明:
file
:这是要执行的新程序的名称。与 execl()
不同,execlp()
会在 PATH
环境变量所指定的目录中查找这个程序。arg
:这是传递给新程序的参数列表。参数列表以 NULL
结束。注意,execlp()
函数需要一个以 NULL
结尾的参数列表。第一个参数通常是程序自身的名称。
例如,如果你想使用 execlp()
来执行一个名为 ls
的程序,你可以这样做:
execlp("ls", "ls", "-l", NULL);
这将会执行 ls
程序,并传递 -l
作为参数。NULL
表示参数列表的结束。
如果 execlp()
函数成功,它将不会返回。如果函数返回,那么意味着有错误发生,你可以检查 errno
来查看错误代码。
#include
#include
int main()
{
execlp("ls","ls","-al",NULL);
//带p的,可以使用环境变量PATH,无需写全路径
return 0;
}
execle调用举例如下:
execle()
是一个在 Unix 和 Linux 系统中用于执行新程序的函数。它是 exec
函数族的一部分,这些函数允许一个进程加载和执行一个新的程序,替换当前进程的映像。
execle()
函数的原型如下:
int execle(const char *path, const char *arg, ..., char * const envp[]);
参数说明:
path
:这是要执行的新程序的路径。arg
:这是传递给新程序的参数列表。参数列表以 NULL
结尾。envp
:这是一个环境变量数组,它的最后一个元素必须是 NULL
。这个数组会成为新程序的环境。注意,execle()
函数需要一个以 NULL
结尾的参数列表。第一个参数通常是程序自身的名称。
例如,如果你想使用 execle()
来执行一个位于 /bin/ls
的程序,并传递一个新的环境变量,你可以这样做:
char *const envp[] = {"PATH=/usr/bin", NULL};
execle("/bin/ls", "ls", "-l", NULL, envp);
这将会执行 ls
程序,并传递 -l
作为参数。NULL
表示参数列表的结束。envp
是新的环境变量数组。
如果 execle()
函数成功,它将不会返回。如果函数返回,那么意味着有错误发生,你可以检查 errno
来查看错误代码。
#include
#include
int main()
{
// 带e的,需要自己组装环境变量
char *const envp[] = {"PATH=/usr/bin", NULL};
execle("/bin/ls", "ls", "-l", NULL, envp);
return 0;
}
execlv调用举例如下:
execv()
是一个在 Unix 和 Linux 系统中用于执行新程序的函数。它是 exec
函数族的一部分,这些函数允许一个进程加载和执行一个新的程序,替换当前进程的映像。
execv()
函数的原型如下:
int execv(const char *path, char *const argv[]);
参数说明:
path
:这是要执行的新程序的路径。argv
:这是传递给新程序的参数列表。参数列表以 NULL
结尾。注意,execv()
函数需要一个以 NULL
结尾的参数列表。第一个参数通常是程序自身的名称。
例如,如果你想使用 execv()
来执行一个位于 /bin/ps
的程序,你可以这样做:
char *argv[] = {"ps", "-ef", NULL};
execv("/bin/ps", argv);
这将会执行 ps
程序,并传递 -ef
作为参数。NULL
表示参数列表的结束。
如果 execv()
函数成功,它将不会返回。如果函数返回,那么意味着有错误发生,你可以检查 errno
来查看错误代码。
execlvp调用举例如下:
execvp()
是一个在 Unix 和 Linux 系统中常用的函数,它用于从当前进程创建一个新的进程。这个函数需要两个参数:要执行的程序的名称,以及一个包含该程序的参数的字符串数组。
在你的例子中,"ps"
是你想要执行的程序的名称,argv
是一个包含 ps
程序参数的字符串数组。
下面是一个使用 execvp()
的简单示例:
#include
int main() {
char *argv[] = {"ps", "-ef", NULL};
execvp("ps", argv);
return 0;
}
在这个例子中,我们创建了一个新的进程来执行 ps -ef
命令,这个命令会显示系统中所有正在运行的进程的详细信息。NULL
是参数列表的结束标志,必须在参数列表的最后。
请注意,execvp()
不会返回,除非发生错误。如果 execvp()
成功,那么新的程序将开始执行,而原来的进程将不再存在。如果 execvp()
返回,那么意味着有错误发生,你可以使用 perror()
函数来显示错误信息。例如:
#include
#include
int main() {
char *argv[] = {"ps", "-ef", NULL};
// 带p的,可以使用环境变量PATH,无需写全路径
if (execvp("ps", argv) == -1) {
perror("execvp");
}
return 0;
}
在这个例子中,如果 execvp()
失败,perror()
将打印一条错误消息。
execlve调用举例如下:
execve()
是 Unix 和 Linux 系统中的一个函数,它用于从当前进程创建一个新的进程。这个函数需要三个参数:要执行的程序的路径,一个包含该程序的参数的字符串数组,以及一个包含环境变量的字符串数组。
下面是一个使用 execve()
的简单示例:
#include
int main() {
char *argv[] = {"ls", "-l", NULL};
char *envp[] = {"PATH=/usr/bin", NULL};
execve("/bin/ls", argv, envp);
return 0;
}
在这个例子中,我们创建了一个新的进程来执行 ls -l
命令,这个命令会以长格式列出当前目录中的所有文件。NULL
是参数列表和环境变量列表的结束标志,必须在列表的最后。
请注意,execve()
不会返回,除非发生错误。如果 execve()
成功,那么新的程序将开始执行,而原来的进程将不再存在。如果 execve()
返回,那么意味着有错误发生,你可以使用 perror()
函数来显示错误信息。例如:
#include
#include
int main() {
char *argv[] = {"ls", "-l", NULL};
char *envp[] = {"PATH=/usr/bin", NULL};
// 带e的,需要自己组装环境变量
if (execve("/bin/ls", argv, envp) == -1) {
perror("execve");
}
return 0;
}
在这个例子中,如果 execve()
失败,perror()
将打印一条错误消息。希望这个解释对你有所帮助!
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve
,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。下图exec函数族 一个完整的例子:
考虑下面这个与shell典型的互动:
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。
所以要写一个shell,需要循环以下过程:
#include
#include
#include
#include
#include
#include
#include
#define MAX_CMD 1024
char command[MAX_CMD];//接受命令的数组
int do_face()
{
memset(command, 0x00, MAX_CMD);
//将command指向的内存区域的前 MAX_CMD 个字节设置为 0x00 的低 8 位,即command数组全为0
printf("myshell$ ");
fflush(stdout);//清空缓冲区到stdout(显示屏)。
if (scanf("%[^\n]%*c", command) == 0)
{
getchar();//用于从标准输入读取一个字符。
return -1;
}
return 0;
}
char **do_parse(char *buff)
{
int argc = 0;
static char *argv[32];
char *ptr = buff;
while(*ptr != '\0')
{
if (!isspace(*ptr))
{
argv[argc++] = ptr;
while((!isspace(*ptr)) && (*ptr) != '\0')
{
ptr++;
}
}else
{
while(isspace(*ptr))
{
*ptr = '\0';
ptr++;
}
}
}
argv[argc] = NULL;
return argv;
}
int do_exec(char *buff)
{
char **argv = {NULL};
int pid = fork();
if (pid == 0)
{
argv = do_parse(buff);
if (argv[0] == NULL)
{
exit(-1);
}
execvp(argv[0], argv);
}
else
{
waitpid(pid, NULL, 0);
}
return 0;
}
int main(int argc, char *argv[])
{
while(1)
{
if (do_face() < 0)
continue;
do_exec(command);
}
return 0;
}
在继续学习新知识前,我们来思考函数和进程之间的相似性。
exec/exit就像call/return一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间。如下图
一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来
返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。