Linux中一个进程是如何被启动起来的?
- 一、进程是怎么启动的?
- 二、进程内存空间分段
- 三、进程的入口函数
- 四、总结
一、进程是怎么启动的?
当一个程序被执行时,怎么看出进程的运行呢?一个进程是怎么启动的?为什么我们在命令行执行程序时敲回车这个进程就启动了呢?
可以使strace
命令看进程的启动。strace
可以跟踪一个进程执行期间的所有系统调用,并显示系统调用及其参数和返回值。它本质上是扮演了一个“中间人”角色,截获进程发出的系统调用请求,并记录其执行过程。
简单来说,strace
就像一个“监视器”,可以让你看到一个进程在后台默默地与操作系统进行的各种交互。了解 strace
的用法,可以参考其官方文档: man strace
。strace
命令输出的信息量比较大,建议使用管道或重定向将输出保存到文件中,方便分析。
因此,使用strace
看我们的进程是怎么启动的:
execve("./server", ["./server"], 0x7ffee9ec6a10 /* 36 vars */) = 0
brk(NULL) = 0x5566b9ac9000
arch_prctl(0x3001 /* ARCH_??? */, 0x7fff025c9720) = -1 EINVAL (Invalid argument)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fe2a66f6000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=16915, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 16915, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fe2a66f1000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libstdc++.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0\0\0\0\0\0\0\0"..., 832) = 832
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=2260296, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 2275520, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fe2a64c5000
mprotect(0x7fe2a655f000, 1576960, PROT_NONE) = 0
mmap(0x7fe2a655f000, 1118208, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x9a000) = 0x7fe2a655f000
mmap(0x7fe2a6670000, 454656, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1ab000) = 0x7fe2a6670000
mmap(0x7fe2a66e0000, 57344, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x21a000) = 0x7fe2a66e0000
mmap(0x7fe2a66ee000, 10432, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fe2a66ee000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0 \0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0"..., 48, 848) = 48
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0I\17\357\204\3$\f\221\2039x\324\224\323\236S"..., 68, 896) = 68
newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=2220400, ...}, AT_EMPTY_PATH) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
mmap(NULL, 2264656, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fe2a629c000
mprotect(0x7fe2a62c4000, 2023424, PROT_NONE) = 0
mmap(0x7fe2a62c4000, 1658880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7fe2a62c4000
mmap(0x7fe2a6459000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bd000) = 0x7fe2a6459000
mmap(0x7fe2a64b2000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x215000) = 0x7fe2a64b2000
mmap(0x7fe2a64b8000, 52816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fe2a64b8000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0\0\0\0\0\0\0\0"..., 832) = 832
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=940560, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 942344, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fe2a61b5000
mmap(0x7fe2a61c3000, 507904, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0xe000) = 0x7fe2a61c3000
mmap(0x7fe2a623f000, 372736, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x8a000) = 0x7fe2a623f000
mmap(0x7fe2a629a000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0xe4000) = 0x7fe2a629a000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0\0\0\0\0\0\0\0"..., 832) = 832
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=125488, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 127720, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fe2a6195000
mmap(0x7fe2a6198000, 94208, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7fe2a6198000
mmap(0x7fe2a61af000, 16384, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a000) = 0x7fe2a61af000
mmap(0x7fe2a61b3000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1d000) = 0x7fe2a61b3000
close(3) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fe2a6193000
arch_prctl(ARCH_SET_FS, 0x7fe2a61943c0) = 0
set_tid_address(0x7fe2a6194690) = 41573
set_robust_list(0x7fe2a61946a0, 24) = 0
rseq(0x7fe2a6194d60, 0x20, 0, 0x53053053) = 0
mprotect(0x7fe2a64b2000, 16384, PROT_READ) = 0
mprotect(0x7fe2a61b3000, 4096, PROT_READ) = 0
mprotect(0x7fe2a629a000, 4096, PROT_READ) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fe2a6191000
mprotect(0x7fe2a66e0000, 45056, PROT_READ) = 0
mprotect(0x5566b8336000, 4096, PROT_READ) = 0
mprotect(0x7fe2a6730000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x7fe2a66f1000, 16915) = 0
getrandom("\xaa\xa1\x40\x73\x3f\x72\xa2\x6e", 8, GRND_NONBLOCK) = 8
brk(NULL) = 0x5566b9ac9000
brk(0x5566b9aea000) = 0x5566b9aea000
futex(0x7fe2a66ee77c, FUTEX_WAKE_PRIVATE, 2147483647) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
exit_group(0) = ?
+++ exited with 0 +++
这里可以看到一个这样一段打印:
execve("./server", ["./server"], 0x7ffee9ec6a10 /* 36 vars */) = 0
执行的时候执行了 “./server” 这个进程,后面的0x7ffee9ec6a10 /* 36 vars */
是对应的命令行参数或程序的环境变量列表,这里包含 36 个变量。
brk(NULL) = 0x5566b9ac9000
调整程序数据段的大小并返回当前数据段的地址。mmap
内存映射将文件或设备映射到进程的地址空间。
所以,从这个系统调用可以清楚的分析出,进程在启动的时候是由 shell 命令启动的,shell也是一个进程,这个进程里通过execve()
函数执行了我们的 “./server” 程序。
二、进程内存空间分段
在了解一个进程是如何执行之前,需要清楚进程的内存空间分区。进程的内存分配包含:堆 (heap)、栈 (stark)、全局静态区(.bss、.data)、文字常量区(.rodata)、代码段。
堆栈区在编译时是不会给他们开辟并占用文件空间的,堆栈段是在进程执行过程中动态开辟的内存空间。
程序在编译时, 链接器会寻找一个叫做 _start
的函数,这个函数是程序真正的入口点,它通常位于 C 标准库中。_start
函数会调用 main()
函数,并传递程序的命令行参数和环境变量。
进程启动时,操作系统通过 execve
系统调用加载可执行文件,并从 _start
函数开始执行。_start
函数会找到 main
函数的地址,并将控制权交给它,从而执行 main
函数。
三、进程的入口函数
为什么启动函数是main()
函数呢?
因为在 C 语言的发展初期,main()
函数被选定为程序的入口点,并被广泛接受为标准。 编译器会将 main()
函数编译成可执行文件的一部分,链接器会将 main()
函数作为程序的入口点,并根据 main()
函数的地址设置程序执行的起始位置。
在执行的时候,进程中有一个叫做代码段的区域。进程执行的时候,编译器会教它从代码段里面找到main函数,而且命名规则是确定的,所以这个函数的入口就叫做main函数。注意,也就是说应用程序的入口函数是main函数,在Linux内核(Kernel)中是没有main函数的,只有应用程序的入口函数是规定从main函数开始执行,对于操作系统而言,每个进程是都是它的一个模块,只是这些进程的入口函数是main函数。很多的模块开发(比如 NGINX的一个模块或者Linux kernel的一个模块),其入口函数都是规定好的。
虽然 main
函数是 C/C++ 程序的默认入口点,但实际上可以通过一些技术手段来改变入口点,例如使用 __attribute__((constructor))
或 __attribute__((destructor))
关键字来定义在 main
函数之前或之后执行的函数。 但是,这通常是比较高级的应用场景,在大多数情况下,main
函数仍然是程序的入口点。
四、总结
进程启动时操作系统通过 execve
系统调用加载并执行程序,以及使用 strace
工具跟踪系统调用的方法。进程内存空间的划分,包括代码段、数据段、堆、栈、环境变量段和命令行参数段,以及它们各自的特点和用途。 main
函数作为进程入口函数的原因,以及编译器和链接器如何找到并执行 main
函数。