Linux内核(kernel)概念
首先来说kernel与OS的区别:kernel是OS最基础的部分,OS还包括其他比如APACHE等套件。
Linux内核的作用是将应用层的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。 内核启动init程序作为第一个进程。该进程负责进一步的系统初始化操作。init进程是进程树的根,所有的进程都直接或者间接起源于该进程。 Linux进程采用层次结构,每个进程都依赖于一个父进程。 Linux内核的作用是将应用层序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。 内核启动init程序作为第一个进程。该进程负责进一步的系统初始化操作。init进程是进程树的根,所有的进程都直接或者间接起源于该进程。
Linux内核的任务:
1.从技术层面讲,内核是硬件与软件之间的一个中间层。作用是将应用层序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。
2.从应用程序的层面讲,应用程序与硬件没有联系,只与内核有联系,内核是应用程序知道的层次中的最底层。在实际工作中内核抽象了相关细节。
3.内核是一个资源管理程序。负责将可用的共享资源(CPU时间、磁盘空间、网络连接等)分配得到各个系统进程。
4.内核就像一个库,提供了一组面向系统的命令。系统调用对于应用程序来说,就像调用普通函数一样。
内核如何运行程序(以例子驱动理解)
bash shell 以及任何用 C 编程语言编写的程序都是从 main 函数开始的。如果您查看 bash shell 的源代码,您会在 shell.c 源代码文件中找到 main 函数。这个函数在 bash 的主线程循环开始工作之前做了很多不同的事情。例如这个函数:
检查并尝试打开 /dev/tty;
检查在调试模式下运行的 shell;
解析命令行参数;
读取shell环境;
加载.bashrc、.profile等配置文件;
还有更多。
在所有这些操作之后,我们可以看到 reader_loop 函数的调用。这个函数定义在 eval.c 源代码文件中,代表主线程循环,或者换句话说,它读取和执行命令。当 reader_loop 函数进行所有检查并读取给定的程序名称和参数时,它会从 execute_cmd.c 源代码文件调用 execute_command 函数。 execute_command 函数通过函数链调用:
执行命令--> 内部执行命令----> 执行简单命令------> 执行磁盘命令--------> shell execve
进行不同的检查,比如我们是否需要启动 subshell,它是否内置了 bash 函数等。正如我上面已经写的,我们不会考虑与 Linux 内核无关的所有细节。在这个过程的最后,shell_execve 函数调用了 execve 系统调用:
execve (command, args, env);
execve 系统调用具有以下格式,并通过给定的文件名、给定的参数和环境变量执行程序。
int execve(const char *filename, char *const argv [], char *const envp[]);
$ strace ls
execve("/bin/ls", ["ls"], [/* 62 vars */]) = 0
$ strace echo
execve("/bin/echo", ["echo"], [/* 62 vars */]) = 0
$ strace uname
execve("/bin/uname", ["uname"], [/* 62 vars */]) = 0
所以,一个用户应用程序(在我们的例子中是 bash)调用系统调用,正如我们已经知道的,下一步是 Linux 内核。这个系统调用定义在 fs/exec.c 源代码文件中,
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);
}
execve 的实现在这里非常简单,我们可以看到它只返回 do_execve 函数的结果。 do_execve 函数定义在同一个源代码文件中,并做以下事情:
使用给定的参数和环境变量初始化用户空间数据上的两个指针;
返回 do_execveat_common 的结果。
实现:
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
do_execveat_common 函数执行主要工作 - 它执行一个新程序。
1.调用unshare_files()为进程复制一份文件表
2.调用kzalloc()分配一份structlinux_binprm结构体
3.调用open_exec()查找并打开二进制文件
4.调用sched_exec()找到最小负载的CPU,用来执行该二进制文件
5.根据获取的信息,填充structlinux_binprm结构体中的file、filename、interp成员
6.调用bprm_mm_init()创建进程的内存地址空间,为新程序初始化内存管理.并调用init_new_context()7.检查当前进程是否使用自定义的局部描述符表;如果是,那么分配和准备一个新的LDT
7.填充structlinux_binprm结构体中的argc、envc成员
8.调用prepare_binprm()检查该二进制文件的可执行权限;最后,kernel_read()读取二进制文件的头128字节(这些字节用于识别二进制文件的格式及其他信息,后续会使用到)
9.调用copy_strings_kernel()从内核空间获取二进制文件的路径名称
10.调用copy_string()从用户空间拷贝环境变量和命令行参数
至此,二进制文件已经被打开,struct linux_binprm结构体中也记录了重要信息, 内核开始调用exec_binprm执行可执行程序
此函数采用类似的参数集,但正如您所见,它采用五个参数而不是三个参数。
第一个参数是代表我们应用程序目录的文件描述符,在我们的例子中 AT_FDCWD 意味着给定的路径名是相对于调用进程的当前工作目录解释的。
第五个参数是标志。 在我们的例子中,我们将 0 传递给 do_execveat_common。 我们将在下一步中检查,以便稍后看到。
if (IS_ERR(filename))
return PTR_ERR(filename);
if ((current->flags & PF_NPROC_EXCEEDED) &&
atomic_read(¤t_user()->processes) > rlimit(RLIMIT_NPROC)) {
retval = -EAGAIN;
goto out_ret;
}
current->flags &= ~PF_NPROC_EXCEEDED;
如果这两个检查成功,我们在当前进程的标志中取消设置 PF_NPROC_EXCEEDED 标志以防止 execve 失败。 可以看到,下一步我们调用了定义在 kernel/fork.c 中的 unshare_files 函数,并取消共享当前任务的文件并检查该函数的结果:
retval = unshare_files(&displaced);
if (retval)
goto out_ret;
我们需要调用这个函数来消除 execve’d 二进制文件描述符的潜在泄漏。 在下一步中,我们开始准备由 struct linux_binprm 结构(在 include/linux/binfmts.h 头文件中定义)表示的 bprm。 linux_binprm 结构用于保存加载二进制文件时使用的参数。 例如,它包含 vma 字段,该字段具有 vm_area_struct 类型并表示在我们的应用程序将被加载的给定地址空间中连续间隔上的单个内存区域,作为二进制文件的内存描述符的 mm 字段,指向内存顶部的指针和许多其他 不同的领域。
首先我们使用 kzalloc 函数为这个结构分配内存并检查分配的结果:
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
goto out_files;
在此之后,我们开始通过调用 prepare_bprm_creds 函数来准备 binprm 凭证:(linux_binprm是定义在include/linux/binfmts.h中, 用来保存要要执行的文件相关的信息, 包括可执行程序的路径, 参数和环境变量的信息)