问题
strace 输出中的 execve(...) 究竟是什么?
进程生命周期
操作系统内部定义了进程的不同状态
Linux 进程基本概念
进程是 Linux 任务的执行单元,也是 Linux 系统资源的分配单元
每个 Linux 应用程序运行后由一个或多个进程构成
每个 Linux 进程可以执行一个或多个程序
Linux 进程有多种不同状态 (即:Linux 进程有不同的活法)
Linux 进程生命周期
可中断状态表示进程可以接收到内核给它发出的信号,不可中断状态表示进程不接收内核给它发出的信号
僵尸状态表示进程的生命周期已经结束了,但内核还残留着这个进程的资源
我们查看下当前的命令行所运行的进程
ps ax 命令查看每个运行进程的信息,grep "pts/2" 用来筛选当前命令行所运行的进程
第一列是每个进程的进程号,第三列是每个进程的状态
下图包含了进程的状态信息
S 表示该进程处于可中断的阻塞状态,R 表示该进程处于运行或就绪状态
s 表示该进程是会话组长,+ 表示该进程在前台进程组里
Linux 进程必知必会
每一个进程都有一个唯一的标识 (PID)
每个进程都是由另一个进程创建而来 (即:父进程)
getpid() 用于获取当前进程的进程号,getppid() 用于获取当前进程的父进程号
Linux 进程树
整个 Linux 系统的所有进程构成一个树状结构
树根由内核自动创建即:IDLE (PID = 0)
系统中的第一个进程是 init / systemd (PID = 1)
0 号进程创建 1 号进程,1 号进程负责完成内核部分初始化工作
1 号进程加载执行初始化程序,演变为用户态 1 号进程
我们来查看下当前命令行进程的进程树
pstree 是查看当前系统进程树的命令,-p 参数会展示出每个进程的 pid,-s 参数表示指定查看某个进程,$$ 代表当前进程的 pid
Linux 进程创建
pid_t fork(void);
- 通过当前进程创建新进程,当前进程为父进程,新进程为子进程
int execve(const char* pathname, char* const argv[], char* const envp[]);
- 在当前进程中执行 pathname 指定的程序代码
fork() 的工作方式
为子进程申请内存空间,并将父进程数据完全复制到子进程空间中
两个进程中的程序执行顺序完全一致 (fork() 函数调用位置)
不同之处:
- 父进程:fork() 返回子进程 pid
- 子进程:fork() 返回0
- 通过 fork() 返回值判断父子进程,执行不同代码
fork() 初探
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
static int g_global = 0;
int main()
{
int pid = 0;
printf("Hello World!\n");
printf("current pid = %d\n", getpid());
if( (pid = fork()) > 0 )
{
g_global = 1;
usleep(100);
printf("child pid = %d\n", pid);
printf("%d g_global = %d\n", getpid(), g_global);
}
else
{
g_global = 10;
printf("parent pid = %d\n", getppid());
printf("%d g_global = %d\n", getpid(), g_global);
}
return 0;
}
程序运行结果如下图所示
fork() 前,打印当前进程的 pid 为 6619
fork() 后,当前进程会创建出和自己一样的子进程,随后,父进程和子进程会同时运行,通过 fork() 的返回值可以判断出父进程和子进程,由于父进程 sleep 100us,所以子进程会先打印,然后父进程再打印
思考
如何理解 "每个 Linux 进程可以执行一个或多个程序"?
每个 Linux 进程 可以读取一个或多个程序的代码和数据,从而来执行程序
execve(...) 的工作方式
根据参数路径加载可执行程序
通过可执行程序信息构建进程数据,并写入当前进程
将程序执行位置重置到入口地址处 (即:main())
execve(...) 将重置当前进程空间 (代码 & 数据) 而不会创建新进程
execve 初探
helloworld.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("exec = %d, %s\n",
getpid(), "Hello World!");
return 0;
}
test.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#define EXE "helloworld.out"
int main()
{
char* args[] = {EXE, NULL};
printf("begin\n");
printf("pid = %d\n", getpid());
execve(EXE, args, NULL);
printf("end\n");
return 0;
}
我们将 helloworld.c 编译为 helloworld.out,将 test.c 编译为 test.out,来查看运行结果
execve 不会创建新的进程,而是将当前进程的进程数据替换为 helloworld.out 的进程数据,并执行,所以执行完 helloworld.out后就结束了,不会打印 end
我们可以 fork 子进程来执行 execve,这样当前进程的进程数据就不会被替换了
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#define EXE "helloworld.out"
int create_process(const char* pathname, char* const argv[])
{
int ret = 0;
if((ret = fork()) == 0)
{
execve(pathname, argv, NULL);
}
return ret;
}
int main()
{
char* args[] = {EXE, NULL};
printf("begin\n");
printf("pid = %d\n", getpid());
printf("child pid = %d\n", create_process(EXE, args));
printf("end\n");
return 0;
}
程序执行结果如下图所示
通过 fork 子进程的方式来执行 execve,这样父进程的数据不会被替换,父进程成功打印出 end