理解Linux系统: 进程

2023-07-10,,

Linux内核版本: 2.6.11.12

编写代码: 创建进程

创建进程使用fork系统调用,官方文档对于fork的描述:

fork()  creates  a new process by duplicating the calling process.  The
new process is referred to as the child process. The calling process
is referred to as the parent process. The child process and the parent process run in separate memory spaces.
At the time of fork() both memory spaces have the same content. Memory
writes, file mappings (mmap(2)), and unmappings (munmap(2)) performed
by one of the processes do not affect the other.
...

fork会创建一个新的进程,通过复制当前调用的进程。这个新的进程被称作子进程,当前调用的进程被称作父进程。子进程和父进程会运行两个不同的内存空间,在fork被调用时,父子进程的地址空间拥有相同的内容(或者叫上下文?),自fork之后父子进程便是隔离的,不相互干扰的。

实例代码 process.c:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h> extern int create_process(char *program,char** arg_list); int create_process(char* program,char** arg_list)
{
pid_t child_pid;
child_pid = fork(); // 进程分叉
if (child_pid) {
return child_pid;
} else {
execvp(program,arg_list); // execute a file
abort(); // cause abnormal process termination
}
}

调用fork之后,父子进程则分道扬镳,通过fork的返回值来判断是哪一个进程,在父进程中fork会返回子进程的ID,在子进程中,fork会返回0。在子进程中使用execvp运行一个新的程序,如下创建第二个文件来调用:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h> extern int create_process(char *program,char ** arg_list); int main(void)
{
char* arg_list[] = {
"ls",
"-l",
};
create_process("ls",arg_list);
return 0;
}

编译: 程序的二进制格式

CPU无法直接执行文本文件中的内容,能够执行的只有二进制指令,因此要将C语言代码编译为二进制程序,二进制程序具有严格的格式,称作ELF,如下是文本文件编译为二进制格式的过程:

如下进行编译:

$ gcc -c -fPIC process.c
$ gcc -c -fPIC main.c

在编译前,先做预处理工作,然后才是真正的编译过程,最终编译为.o文件,这是ELF的第一种类型: 可重定位文件

格式如下:

在源代码的/include/linux/elf.h目录下,定义了两个结构体,对应32位和64位的ELF文件头:

typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr; typedef struct elf64_hdr {
unsigned char e_ident[16]; /* ELF "magic number" */
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;

ELF文件头用于描述整个ELF文件

如上图所示,ELF文件是由一个个节(section)组成的

名称 含义
.text 放置可执行代码
.data 已经初始化的全局变量
.rodata 只读数据
.bss 未初始化的全局变量,运行时置0
.symtab 符号表,记录函数和变量
.strtab 字符串表、字符串常量和变量名

程序要运行起来,编译好的代码和变量将会被加载到一定位置。例如调用一个函数时,就是跳到这个函数所在的代码位置执行。但.o文件并不是一个可以直接运行的程序,其中只是部分代码片段。其中的create_process函数,将来会被谁调用,在哪里调用是不清楚的,因此被加载到内存的哪里是不确定的,但必须是可重定位的。可以将一系列.o文件归档为.a静态链接库文件: ar cr libprocess.a process.o,编译: gcc -o process main.o -L. -lprocess

编译后形成的二进制文件叫可执行文件,是ELF的第二种格式,格式如下:

该文件是可以立即加载到内存运行的文件,其中的section被分为要加载到内存里面的代码段、数据段和无须家早到内存中的部分,将小的section合成了大的段segment,并且在最前面加一个段头表(Segment Header Table)。同样定义在elf.h中:

typedef struct elf32_phdr{
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr; typedef struct elf64_phdr {
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment, file & memory */
} Elf64_Phdr;

在ELF头中,有一项e_entry,是一个虚拟地址,是这个程序运行的入口

静态链接库一旦被链接,代码和变量的section都被合并,因而程序运行时就不依赖于这个库是否存在。但这样有一个缺点,相同的代码段,如果被多个程序使用,在内存中就有多份,而且一旦静态链接库更新了,如果二进制执行文件不重新编译,也不随着更新。

动态链接库(Shared Libraries),不仅仅是一组对象文件的简单归档,而是多个对象文件的重新组合,可以被多个程序共享,编译动态链接库:gcc -shared -fPIC -o libprocess.so process.o,编译可执行文件: gcc -o process main.o -L. -lprocess

当一个动态链接库被链接到一个程序文件中,最后程序文件并不包括动态链接库中代码,而仅仅包括对动态链接库的引用,并且不保存动态链接库的全路径,仅仅保存动态链接库的名称。

当运行这个程序时,首先寻找动态链接库,然后加载它。默认情况下,系统在/lib和/usr/lib文件夹下寻找动态链接库,如果找不到就报错,可以设定LD_LIBRARY_PATH环境变量,程序运行时会在此环境变量指定的文件夹下寻找动态链接库

运行程序为进程

运行程序时,要将ELF格式的二进制文件加载到内存中,内核中有这样一个数据结构,用于定义加载二进制文件的方法:

struct linux_binfmt {
struct list_head lh;
struct module *module;
int (*load_binary)(struct linux_binprm *);
int (*load_shlib)(struct file *);
int (*core_dump)(struct coredump_params *cprm);
unsigned long min_coredump; /* minimal dump size */
} __randomize_layout;

对于ELF文件格式有对应的实现:

static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
.load_shlib = load_elf_library,
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE,
};

进程树

所有的进程都是从父进程 fork 过来的,就有一个祖宗进程,这就是系统启动的 init 进程。

系统启动之后,init 进程会启动很多的 daemon 进程,为系统运行提供服务,然后就是启动 getty,让用户登录,登录后运行 shell,用户启动的进程都是通过 shell 运行的,从而形成了一棵进程树。可以通过 ps -ef 命令查看当前系统启动的进程,会发现有三类进程:

$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 7月09 ? 00:00:10 /sbin/init splash
root 2 0 0 7月09 ? 00:00:00 [kthreadd]
root 3 2 0 7月09 ? 00:00:00 [rcu_gp]
root 4 2 0 7月09 ? 00:00:00 [rcu_par_gp]
root 6 2 0 7月09 ? 00:00:00 [kworker/0:0H-events_highpri
root 9 2 0 7月09 ? 00:00:00 [mm_percpu_wq]
root 10 2 0 7月09 ? 00:00:00 [rcu_tasks_rude_]
root 11 2 0 7月09 ? 00:00:00 [rcu_tasks_trace]
root 12 2 0 7月09 ? 00:00:00 [ksoftirqd/0]
root 13 2 0 7月09 ? 00:00:22 [rcu_sched]
root 14 2 0 7月09 ? 00:00:00 [migration/0]
root 15 2 0 7月09 ? 00:00:00 [idle_inject/0]
.....
root 127746 2 0 08:07 ? 00:00:00 [kworker/14:1-events]
root 127748 2 0 08:08 ? 00:00:00 [kworker/15:0-events]
root 127774 2 0 08:09 ? 00:00:00 [kworker/u32:2-i915]
hwx 127832 1649 0 08:10 ? 00:00:00 /usr/libexec/tracker-store
root 127862 2 0 08:10 ? 00:00:00 [kworker/u33:1]
root 127876 2 0 08:10 ? 00:00:00 [kworker/12:1-cgroup_destroy
hwx 127922 1649 0 08:11 ? 00:00:00 /usr/libexec/tracker-extract
hwx 127953 1649 6 08:11 ? 00:00:00 /usr/libexec/gnome-terminal-
hwx 127961 127953 0 08:11 pts/0 00:00:00 bash
hwx 127967 127961 0 08:11 pts/0 00:00:00 ps -ef

PID 1 的进程就是 init 进程 systemd,PID 2 的进程是内核线程 kthreadd。其中用户态的不带中括号,内核态的带中括号。

接下来进程号依次增大,但是会看所有带中括号的内核态的进程,祖先都是 2 号进程。而用户态的进程,祖先都是 1 号进程。tty 那一列,是问号的,说明不是前台启动的,一般都是后台的服务。

理解Linux系统: 进程的相关教程结束。

《理解Linux系统: 进程.doc》

下载本文的Word格式文档,以方便收藏与打印。