进程

源码

//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 != 0)
    {
        return child_pid;
    }
    else 
    {
        execvp (program, arg_list);
        abort ();
    }
}

在这里,我们创建的子程序运行了一个最最简单的命令 ls

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

编译

CPU 看不懂源码里的函数,命令,CPU 只认二进制数据,所以源码需要翻译成01二进制数据,这个过程就是**编译(Compile)**的过程。

编译出的文件好比一个公司的项目执行计划书,你要把一个项目执行好,计划书得有章法,有一定格式。在 Linux 下,二进制程序也有这样的格式,叫ELF(Executeable and Linkable Format,可执行与可链接格式),这个格式可以根据编译的结果不同,分为不同的格式。

ELF-可重定位文件

下图展示了如何从源码到二进制文件的转化

Responsive Image

gcc -c -fPIC process.c
gcc -c -fPIC createprocess.c

-fPIC作用于编译阶段,告诉编译器产生与位置无关代码 (Position-Independent Code)。产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。

在编译的时候,先做预处理工作,例如将头文件嵌入到正文中,将定义的宏展开,然后就是真正的编译过程,最终编译成为.o文件,这就是ELF的第一种类型,可重定位文件(Relocatable File)。文件格式如下,

Responsive Image

ELF 文件的头是用于描述整个文件的。这个文件格式在内核中有定义,分别为 struct elf32_hdrstruct elf64_hdr

section内容
.text放编译好的二进制可执行代码
.data已经初始化好的全局变量(临时变量放在栈里)
.rodata只读数据,例如字符串常量、const 的变量
.bss未初始化全局变量,运行时会置 0
.symtab符号表,记录的则是函数和变量
.strtab字符串表、字符串常量和变量名

第一种 ELF 文件叫可重定位文件,为啥可重定位?我们可以想象一下,这个编译好的代码和变量,将来加载到内存里面的时候,都是要加载到一定位置的。比如说,调用一个函数,其实就是跳到这个函数所在的代码位置执行;再比如修改一个全局变量,也是要到变量的位置那里去修改。但是现在这个时候,还是.o文件,不是一个可以直接运行的程序,这里面只是部分代码片段。

例如这里的 create_process函数,将来被谁调用,在哪里调用都不清楚,就更别提确定位置了。所以,.o里面的位置是不确定的,但是必须是可重新定位的,因为它将来是要做函数库的嘛,就是一块砖,哪里需要哪里搬,搬到哪里就重新定位这些代码、变量的位置。

ELF-可执行文件

要让create_process这个函数作为库文件重用,需要将其形成库文件,最简单的类型是静态链接库.a文件,它将一系列.o文件归档为一个文件。使用ar命令创建.a文件。使用方法看这里

ar cr libstaticprocess.a process.o

虽然这里 libstaticprocess.a 里面只有一个.o,但是实际情况可以有多个.o。当有程序要使用这个静态连接库的时候,会将.o 文件提取出来,链接到程序中。

gcc -o staticcreateprocess createprocess.o -L. -lstaticprocess

-L表示在当前目录下找.a文件,-lstaticprocess 会自动补全文件名,比如加前缀 lib,后缀.a,变成 libstaticprocess.a,找到这个.a文件后,将里面的 process.o 取出来,和 createprocess.o 做一个链接,形成二进制执行文件 staticcreateprocess

在链接过程中,重定位就起作用了,在createprocess.o里调用了create_process函数,但是不能确定位置,现在将process.o合并进来,就知道位置了。

Responsive Image

这个格式和.o 文件大致相似,还是分成一个个的 section,并且被节头表描述。只不过这些section 是多个.o 文件合并过的。但是这个时候,这个文件已经是马上就可以加载到内存里面执行的文件了,因而这些 section 被分成了需要加载到内存里面的代码段数据段不需要加载到内存里面的部分,将小的 section 合成了大的段 segment,并且在最前面加一个段头表(Segment Header Table)。

在代码里面的定义为 struct elf32_phdrstruct elf64_phdr,这里面除了有对于段的描述之外,最重要的是 p_vaddr,这个是这个段加载到内存的虚拟地址。

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

ELF-共享对象文件

静态库一旦被链接,代码和变量的section会被合并,所以运行时不依赖静态库文件,但是缺点就是,相同代码段被多个程序使用,在内存里会有多份,而且静态库更新需要重新编译

因而就出现了另一种,动态链接库(Shared Libraries),不仅仅是一组对象文件的简单归档,而是多个对象文件的重新组合,可被多个程序共享。

gcc -shared -fPIC -o libdynamicprocess.so process.o

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

gcc -o dynamiccreateprocess createprocess.o -L. -ldynamicprocess

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

# export LD_LIBRARY_PATH=.
# ./dynamiccreateprocess
# total 40
-rw-r--r--. 1 root root 1572 Oct 24 18:38 CentOS-Base.repo
......

动态链接库,就是ELF的第三种类型,共享对象文件(Shared Object)。

文件格式和上两种文件稍有不同,首先,多了一个.interpSegment,这里面是 ld-linux.so,这是动态链接器,也就是说,运行时的链接动作都是它做的。

另外,ELF文件中还多了两个section,一个是.plt过程链接表(Procedure Linkage Table,PLT),一个是。got.plt全局偏移量表(Global Offset Table,GOT)。

运行

在内核中,有linux_binfmt elf_format数据结构定义了加载 ELF 的方法,使用load_elf_binary加载二进制文件,该函数由do_execve调用,学过系统调用知道exec调用了do_execve函数。所以流程为

exec->do_execve->load_elf_binary

进程树

所有进程都是从父进程 fork 来的,祖宗进程就是init 进程。

系统启动之后,init 进程会启动很多的daemon 进程,为系统运行提供服务,然后就是启动 getty,让用户登录,登录后运行 shell,用户启动的进程都是通过 shell运行的,从而形成了一棵进程树。

我们可以通过 ps -ef命令查看当前系统启动的进程,我们会发现有三类进程。PID 1 的进程就是我们的init进程 systemdPID 2 的进程是内核线程 kthreadd

内核态进程的PPID祖先进程都是 2 号进程,用户态进程祖先进程都是 1 号进程,tty列是问号的,说明是后台服务进程。

Responsive Image

进程数据结构

在 Linux 里面,无论是进程还是线程,到了内核里面,我们统一都叫任务(Task),由一个统一的结构task_struct进行管理。

Responsive Image

每个任务应该包含的字段:

任务 ID

pid_t pid; #process id
pid_t tgid; #thread group ID
struct task_struct *group_leader; 

为何要有这么多 ID,一个不够吗?

  • 可以方便任务展示,比如在命令行中 ps 显示所有进程,只显示pid_t pid,而不会把所有内部线程摊开展示,这样太碍眼。
  • 方便下达命令,当我 kill 一个进程时,我们是对整个进程发送信号,但是有时候一些命令只需要对某个线程发送信号。

信号处理

/* Signal handlers: */
struct signal_struct    *signal;
struct sighand_struct    *sighand;
sigset_t      blocked;
sigset_t      real_blocked;
sigset_t      saved_sigmask;
struct sigpending    pending;
unsigned long      sas_ss_sp;
size_t        sas_ss_size;
unsigned int      sas_ss_flags;

这里定义了哪些信号被阻塞暂不处理(blocked),哪些信号尚等待处理(pending),哪些信号正在通过信号处理函数进行处理(sighand)。处理的结果可以是忽略,可以是结束进程等等。

任务状态


volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */

int exit_state;

unsigned int flags;

state可取值定义如下

/* Used in tsk->state: */
#define TASK_RUNNING                    0
#define TASK_INTERRUPTIBLE              1
#define TASK_UNINTERRUPTIBLE            2
#define __TASK_STOPPED                  4
#define __TASK_TRACED                   8
/* Used in tsk->exit_state: */
#define EXIT_DEAD                       16
#define EXIT_ZOMBIE                     32
#define EXIT_TRACE                      (EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_DEAD                       64
#define TASK_WAKEKILL                   128
#define TASK_WAKING                     256
#define TASK_PARKED                     512
#define TASK_NOLOAD                     1024
#define TASK_NEW                        2048
#define TASK_STATE_MAX                  4096

可以发现 Linux 通过 bitset 方式设置状态,当前什么状态,哪一位就置 1。

进程调度

进程的状态切换往往涉及调度,下面这些字段都是用于调度的。

// 是否在运行队列上
int               on_rq;
// 优先级
int               prio;
int               static_prio;
int               normal_prio;
unsigned int      rt_priority;
// 调度器类
const struct sched_class  *sched_class;
// 调度实体
struct sched_entity       se;
struct sched_rt_entity    rt;
struct sched_dl_entity    dl;
// 调度策略
unsigned int      policy;
// 可以使用哪些 CPU
int            nr_cpus_allowed;
cpumask_t      cpus_allowed;
struct sched_info    sched_info;

运行统计信息

u64        utime;// 用户态消耗的 CPU 时间
u64        stime;// 内核态消耗的 CPU 时间
unsigned long      nvcsw;// 自愿 (voluntary) 上下文切换计数
unsigned long      nivcsw;// 非自愿 (involuntary) 上下文切换计数
u64        start_time;// 进程启动时间,不包含睡眠时间
u64        real_start_time;// 进程启动时间,包含睡眠时间

进程亲缘关系

进程有棵进程树,所以有亲缘关系。

struct task_struct __rcu *real_parent; /* real parent process */
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
struct list_head children;      /* list of my children */
struct list_head sibling;       /* linkage in my parent's children list */

通常情况下,real_parentparent 是一样的,但是也会有另外的情况存在。例如,bash 创建一个进程,那进程的 parent 和 real_parent 就都是 bash。如果在 bash 上使用 GDBdebug 一个进程,这个时候 GDBreal_parentbash 是这个进程的 parent

进程权限

/* Objective and real subjective task credentials (COW): */
const struct cred __rcu         *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu   

real_cred 就是说明谁能操作我这个进程,而 cred 就是说明我这个进程能够操作谁。

总结到一起,task_struct结构图如下,

Responsive Image