系统调用有了之后,就可以批量的接项目了,对应到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的内存中执行