系统调用有了之后,就可以批量的接项目了,对应到Linux操作系统,就是创建进程

如何创建进程呢?如何让我们自定义代码去创建进程呢?

在Linux上写程序和编译程序,需要对应的开发套件,使用对应的命令,获取套件

yum -y groupinstall “Development Tools”

在Window上的程序,往往都会保存为.h或者.c文件,这些其实本质上就是文本文件,在Linux上,对应的直接使用Vim进行创建编辑即可

我们创建一个文件,里面有着对应的创建进程逻辑

名为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 ();

}

}

上面利用fork系统调用,然后加上if-else,根据fork的返回值不同,来让子进程执行execvp

然后创建一个调用文件

然后是第二个文件,进行上面函数的调用,名为create_process.o

#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;

}

进行编译:如何将程序代码进行执行?

上面的文件只是文本,并不是真正意义上的项目执行计划书,我们需要编译为对应的项目执行计划书

对应的项目执行计划书,在Linux上必然是二进制的,也必须会有一个严格的格式,此格式称为ELF(可执行与可链接格式),这个格式根据编译的结果不同,分为不同的格式

图片

上面就是本章的重点,我们需要讲解的东西

我们上面中,.c文件,就是源文件

我们需要编译对应的程序

gcc -c -fPIC process.c

gcc -c -fPIC createprocess.c

上面的include的部分即为头文件

编译的时候,将头文件嵌入到正文中,然后将宏展开,执行编译,称为.o文件,就是ELF的可重定义文件

文件内部格式是 图片

ELF头是描述整个文件的,在内核中有定义,分为struct elf32_hdr,struct elf64_hdr

然后多个section

其中有一些全局变量/静态变量等

.text 二进制可执行代码

.data 初始化的全局变量

.rodata 只读数据

.bss 未初始化全局变量

.symtab 符号表,记得是函数和变量

.strtab 字符串表,字符串常量和变量名

这里为何只有全局变量,是因为局部变量放在了栈空间中,方便运行时候随时释放

最后的节头部表,每个section都有一项,里面定义了struct elf32_shdr和struct elf64_shdr,在ELF的头中,有描述这个文件节头部表的位置

在ELF中,描述了多个表项信息等

然后是可重定位,啥是可重定位呢?编译号的代码和变量都会加载到内存

调用函数,也是到对应函数的代码位置执行,修改全局变量,也是变量位置修改,但是在VFS中,就是一个.o文件,并不可以直接运行

所以.o的位置是不确定的,必须要可重新定位的,未来是函数库的一个砖,哪里需要往哪搬

上面的第二个函数需要调用第一个函数,但是实现,谁知道第一个函数的位置,只好在rel.text中标注,函数需要重定位

想要上面的create_prcess函数可以被重用,就不能以.o的文件形式存在,而是称为一个静态链接库 .a文件,将一系列的 .o归档为一个文件,使用ar进行创建

ar cr libstaticprocess.a process.o

ar文件中包含了对应的.o文件,在需要使用的时候

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

将利用这个文件,进行相关调用,先找到对应的.a文件,然后取出process.o,和createprocess.o进行链接,成为一个二进制执行文件

这就是可执行文件,ELF的第二种格式

图片

还是分为了多个section,有对应的节头表描述,不过section由多个.o文件合并,

这个文件是马上可以加载到内存中执行的文件了,所以分为了代码段,数据段,不需要加载到内存的部分

小的section合并为了大的segment,最后加上一个段头表

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

静态链接的方式,就使得代码合并了,但是如果相同的代码段,多个程序使用,内存中就有多份,静态链接库被更新了,如果不进行重新编译,不会随着更新

然后就是静态链接库

将原本的对象归档改为了对象文件的重新组合,被多个程序共享

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

创建了一个动态链接库到process.o

一个动态链接库被链接到一个程序文件的时候,最后的程序文件不需要包括动态链接库中的代码,而是包含了对动态链接库的引用,不保存动态链接库的全部路径,而是保存了动态链接库的名称

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

运行的这个程序,找对应的动态链接库,加载其,首先在/lib和/user/lib下找对应的动态链接库,找不到就报错,不过可以设置对应的环境变量

# export LD_LIBRARY_PATH=../dynamiccreateprocess

动态链接库,就是ELF的第三种,共享对象文件

动态链接库出来的二进制文件还是ELF,但是有了改变

多了一个.inerp的Segment,里面有对应的动态链接器

ELF还多了一个section,一个是.plt,过程链接表,一个是got.plt 全局偏移表

对于动态链接,我们编译的时候不知道函数在那里以及去哪里找,于是在PLT中创建了一项 PLT[X]

就是一个代码,就是一个本地代理,于是在PLT中创建了一项PLT[X],类似一个本地代理,在二进制程序中,调用这个代理,获取真正的create_process函数

那么程序运行的流程如下

内核中有对应的数据结构,加载二进制的文件

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,

};

上面有着对应的load_elf_binary,加载内核镜像的时候,也是如下的格式

上面的load_elf_binary是谁调用的呢?

也是对应的do_execve调用的

do_execve的调用栈如下

SYSCALL_DEFINE3(execve,

const char __user *, filename,

const char __user *const __user *, argv,

const char __user *const __user *, envp)

{

return do_execve(getname(filename), argv, envp);

}

exec的调用基本如下

图片

process.c的代码中,创建了ls进程,通过exec

最后说一下进程树

因为所有的进程都是从父进程fork过来的,那么会有一个祖宗进程,就是对应的init进程

图片

对应的1号进程,就是/sbin.init,在centOS7中,我们可以ls看到软链接到systemd的

/sbin/init -> ../lib/systemd/systemd

系统启动后,init会启动很多deamon进程,为系统运行提供服务,启动getty,让用户登录,登录后运行shell

形成进程数

可见的进程树如下

ps -ef 查看当前系统启动的进程,常见的进程分为三类

[root@deployer ~]# ps -ef

UID        PID  PPID  C STIME TTY          TIME CMD

root         1     0  0  2018 ?        00:00:29 /usr/lib/systemd/systemd –system –deserialize 21

root         2     0  0  2018 ?        00:00:00 [kthreadd]

root         3     2  0  2018 ?        00:00:00 [ksoftirqd/0]

root         5     2  0  2018 ?        00:00:00 [kworker/0:0H]

root         9     2  0  2018 ?        00:00:40 [rcu_sched]

……

root       337     2  0  2018 ?        00:00:01 [kworker/3:1H]

root       380     1  0  2018 ?        00:00:00 /usr/lib/systemd/systemd-udevd

root       415     1  0  2018 ?        00:00:01 /sbin/auditd

root       498     1  0  2018 ?        00:00:03 /usr/lib/systemd/systemd-logind

……

root       852     1  0  2018 ?        00:06:25 /usr/sbin/rsyslogd -n

root      2580     1  0  2018 ?        00:00:00 /usr/sbin/sshd -D

root     29058     2  0 Jan03 ?        00:00:01 [kworker/1:2]

root     29672     2  0 Jan04 ?        00:00:09 [kworker/2:1]

root     30467     1  0 Jan06 ?        00:00:00 /usr/sbin/crond -n

root     31574     2  0 Jan08 ?        00:00:01 [kworker/u128:2]

……

root     32792  2580  0 Jan10 ?        00:00:00 sshd: root@pts/0

root     32794 32792  0 Jan10 pts/0    00:00:00 -bash

root     32901 32794  0 00:01 pts/0    00:00:00 ps -ef

PID 1的进程就是init的systemd,

PID 2的进程是内核线程的kthreadd,在内核启动的都见过

然后依次就是用户态的进程

pts的父进程是sshd,bash的父进程是pts ps -ef命令的父进程是bash

那么我们总结一下

我们说了一个进程从代码到二进制运行时的一个过程

首先是文件编译过程,生成so文件和可执行文件,放在硬盘上

然后用户态调用fork,创建另一个进程,进程B的过程中,调用exec

这个调用会通过load_elf_binary方法,将刚刚的可执行文件,加载到进程B的内存中执行

图片

发表评论

邮箱地址不会被公开。 必填项已用*标注