1 进程和程序
可执行的文件,躺在硬盘上的叫程序,运行起来了就叫进程。
从内核 的层面来看,进程由用户内存空间和一系列内核数据结构组成。其中,用户内存空间包含了程序代码和代码使用的变量,内核数据结构用于维护进程的状态信息。这些记录在内核数据结构的信息有:进程标识号IDs、虚拟内存表、打开文件描述符表、信号传递及处理的相关信息、进程资源使用和限制、当前工作目录、环境变量、命令行等等大量的相关信息。
2 进程号和父进程号
定义在”<stdlib.h>“中的系统调用函数getpid()、getppid()。下面是一个简单的测试。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc ,char *argv[])
{
int sleep_seconds=0;
long current_pid = (long) getpid();
long parent_pid = (long) getppid();
printf("程序:%s,PID是:%ld,PPID是:%ld\n",argv[0], current_pid,parent_pid );
if ( argc >= 2 )
{
sleep_seconds = atoi(argv[1]);
}
if ( sleep_seconds > 0)
{
printf("程序:%s,PID是:%ld,正在睡眠,需等%d秒钟\n",argv[0], current_pid,sleep_seconds );
sleep(sleep_seconds);
}
printf("程序:%s,PID是:%ld,PPID是:%ld,运行结束\n",argv[0], current_pid,parent_pid );
return 0;
}
gcc main.c 编程后生成a.out
$ ./a.out 10 &
[1] 9320
程序:./a.out,PID是:9320,PPID是:8518
程序:./a.out,PID是:9320,正在睡眠,需等10秒钟
$ pidof a.out
9320
程序:./a.out,PID是:9320,PPID是:8518,运行结束
[1]+ 已完成 ./a.out 10
你可能会想到,在系统中能运行无穷尽的进程吗?答案当然是否定的,PC功能再强大,内存、硬盘和CPU等算力资源也是有限的。内核 常量ID_MAX定义,32位Linux的进程号上限为32767。(因为:PID_MAX=0x8000,因此进程号的最大值为0x7fff,即32767)。在64位Linux平台上是4194304。具体可用下面的两种命令得到这个值(我的是64位Ubuntu20):
$ cat /proc/sys/kernel/pid_max
4194304
$ sudo sysctl -a | grep pid_max
kernel.pid_max = 4194304
注意,我们创建的进程号一般会大于300,这是因为进程号0-299保留给daemon进程。
由于一般机器不可能同时跑那么多进程+线程,所以32768(32位)/4194304(64位)是肯定够用了,但是系统倾向于分配未使用过的pid给新进程,所以你会发现在正在运行的系统上,有很多低位的pid没有使用,那是因为启动的时候该pid被其它程序用过了,当然,你真有本事用到pid的最大值,系统也有办法解决,那就是从头(低位)搜索未被占用的pid分配给新进程。
除了极少数系统进程外(比如init的进程号是1),一般情况下,程序与进程号没有固定关系。
如果一个父进程终止了,那么子进程将成为”孤儿“,init进程将接管这个子进程。但是在Ubuntu20上,我用终端测试的结果是:父进程终止,子进程也会被强行终止,测试步骤如下:
(1)打开A终端:
$ echo $$
16830 A终端的进程号是16830
$ kolourpaint & 启动一个名为kolourpaint的画图进程
(2)打开B终端:
$ pidof kolourpaint
523663 (kolourpaint的进程号)
$ ps -ef | grep kolourpaint 用这个命令再证实A终端进程与kolourpaint进程是父子关系
songguo+ 523663 16830 0 21:21 pts/0 00:00:00 kolourpaint
(3)关闭A终端,相当于是关闭了父进程
(4)再到B终端中输入
$ pidof kolourpaint 找不到kolourpaint
$ ps -ef | grep kolourpaint 找不到kolourpaint
3 进程内存布局
在32位系统上,进程一旦启动,就会申请4G虚拟地址空间.在64位系统上,会申请256TB虚拟地址空间。进程的空间有以下内存分段:
栈:局部变量、const局部常量、函数参数、返回地址等
堆:动态分配的内存,
BSS段:可读+可写,未初始化/初始化为0的静态变量/全局变量
数据段:可读+可写,初始化为~0的静态变量/全局变量
代码段:只读+可执行,可执行代码、常量(字符串常量;const全局常量;enum常量;#define常量等)
上图对于每个进程的内存布局都是一样的。但地址是虚拟的,从整个系统来看,地址是不相冲突的。比如,每个应用程序都是从0x80480000这个地址开始的,这个地址是一个虚拟地址,它映射到物理内存的不同内存段,所以不会冲突。
4 Linux虚拟地址空间布局
Linux运行在虚拟地址空间,并负责把系统中实际存在的远小于4GB的物理地址空间(物理内存)根据不同需求,映射到整个4GB的虚拟地址空间中。这也就说:同一块物理内存可能映射到多处虚拟内存地址空间上,这正是Linux内存管理职责所在。
虚拟地址通过页表(Page Table)映射到物理内存,页表由操作系统维护并被处理器引用。内核空间在页表中拥有较高特权级,因此用户态程序试图访问这些页时会导致一个页错误(page fault)。
在Linux中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化。
Linux进程在虚拟内存中的标准内存段布局如下图所示:
上图中,用户地址空间中的蓝色条带对应于映射到物理内存的不同内存段,灰白区域表示未映射的部分。这些段只是简单的内存地址范围,与CPU处理器的段没有关系。
上图中Random stack offset和Random mmap offset等随机值意在防止恶意程序。Linux通过对栈、内存映射段、堆的起始地址加上随机偏移量来打乱布局,以免恶意程序通过计算访问栈、库函数等地址。execve(2)负责为进程代码段和数据段建立映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。另外,execve(2)还会将BSS段清零。