[TOC]
Linux内核涉及进程和程序的所有算法都围绕一个名为task_struct的数据结构建立
,该结构定义在/usr/include/sched.h
中;task_struct数据结构提供了两个链表表头,用于实现进程家族关系;
Linux内核把虚拟地址空间划分为两个部分即核心态,用户状态
,两种状态的关键差别在于对高于TASK_SIZE的内存区域的访问
:
Linux进程可以分为实时进程和非实时进程
,硬实时进程的关键特征某些任务必须在指定的时限内完成(严格的时间限制
),而软实时进程是硬实时进程的一种弱化形式。
Linux使用了源于BSD的套接字抽象,而套接字Socket可以看作应用程序、文件接口、内核的网络实现之间的代理;
Linux提供资源限制(resource limit,rlimit)机制对进程使用系统资源施加某些限制。该机制利用了task_struct中的rlimit数组项类型为struct rlimit
。打开文件的数目(RLIMIT_NOFILE,默认限制在1024)。 每用户的大进程数(RLIMIT_NPROC)定义为max_threads/2。max_threads是一个全局变量
,指定了在把八分之一
可用内存用于管理线程信息的情况下可以创建的线程数目。在计算时提前给定了20个线程的小可能内存用量。
(1)伙伴系统:系统中的空闲内存块总是两两分组,每组中的两个内存块称作伙伴
(2)字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。
(3)块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。
(4)抢占式多任务处理(preemptive multitasking):各个进程都分配到一定的时间段可以执行。
(5)完全公平调度器(completely fair scheduler):在内核版本2.6.23开发期间合并进来。
(6)内核抢占(kernel preemption):该选项支持在紧急情况下切换到另一个进程,甚至当前是处于核心态执行系统调用(中断处理期间是不行的)
描述: 从下面一张图看出Linux内核之中都有啥进行简单描述:
WeiyiGeek.
PIPE: 是Uinx/posix中一种进程通讯机制,数据可以通过管道进行传输(实际是进程间的通讯)。 Ulimit: 是Unix/Linux中用于限制资源分配的命令,可以设置系统可以分配的文件句柄数等,例如像Apache之类的服务则需要足够的句柄数才能提供更高的连接数。
Linux 内核中实现6种namespace说明: | Namespace | 内容 | | — | — | |UTS | 主机名与域名| |User| 用户和用户组| |IPC | 信号量、消息队列以及共享内存| |PID | 进程编号| |Network|网络设备、网络栈、端口等| |Mount| 挂载点(文件系统)|
示例1:当前Linux宿主机终端进程对应的namespace信息:
ls -l /proc/$$/ns
总用量 0
lrwxrwxrwx. 1 root root 0 7月 4 23:27 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 root root 0 7月 4 23:27 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 root root 0 7月 4 23:27 net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0 7月 4 23:27 pid -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0 7月 4 23:27 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 7月 4 23:27 uts -> uts:[4026531838]
内核Kernel是Linux的系统重要组成部分,相当于是其心脏;
通过前面的学习与内核升级我们知道Kernel包括以下几个软件包
简述几个软件包的作用:
/usr/include/linux:/usr/include/asm*
等内核头文件。Fedora, Redhat, CentOS
系统;
kernel和kernel-devel
两个rpm,其中kernel rpm包含源文件和头文件(就像2.4下的kernel-source rpm),而kernel-devel则主要是头文件。Linux 内核源代码下载地址 官方: https://mirrors.edge.kernel.org/pub/linux/kernel/ https://git.kernel.org/pub/
国内: https://mirror.bjtu.edu.cn/kernel/linux/kernel/ http://mirrors.163.com/kernel/
kerner-lt # 长期支持版本; Linux内核(任何基于linux的操作系统的核心。) kernel-ml # 主线 mainline; Linux内核(任何基于linux的操作系统的核心。) kernel-ml-devel # 用于构建内核模块以匹配内核的开发包 kernel-ml-doc # 在内核源代码中可以找到各种文档 kernel-ml-headers # 头文件的内核,由glibc的使用 kernel-ml-tools # 用于内核的各种工具 kernel-ml-tools-libs # 内核工具的库 kernel-ml-tools-libs-devel #内核工具库的开发包
kernel-ml-4.20.13-1.el7.elrepo.x86_64 kernel-ml-4.18.1-1.el7.elrepo.x86_64
4 :目前发布的内核主版本。
18 :偶数表示稳定版本,奇数表示开发中版本。
1 :错误修补的次数。
1 :当前这个版本的第 1 次微调 patch
Q: 那么什么是动态库?为什么需要动态库? 答:所谓动态库、静态库,指的是程序编译的链接阶段,链接成可执行文件的方式
补充说明: 90 年代的程序大多使用的是静态链接,因为当时的程序大多数都运行在软盘或者盒式磁带上,而且当时根本不存在标准库。这样程序在运行时与函数库再无瓜葛,移植方便。但对于 Linux 这样的分时系统,会在同一块硬盘上并发运行多个程序,这些程序基本上都会用到标准的 C 库,这时使用动态链接的优点就体现出来了。使用动态链接时,可执行文件不包含标准库文件,只包含到这些库文件的索引。例如,某程序依赖于库文件 libtrigonometry.so 中的 cos 和 sin 函数,该程序运行时就会根据索引找到并加载 libtrigonometry.so,然后程序就可以调用这个库文件中的函数。
Q: 使用动态链接的好处显而易见?
Q: 问:什么是内核线程?
答:内核线程是直接由内核本身启动的进程,它实际上是将内核函数委托给独立的进程,与系统中其他进程“并行”执行
(实际上也并行于内核自身的执行), 内核线程经常称之为(内核)守护进程Deamon
。它们用于执行下列任务。
问:如何启动或者定义一个Linux内核函数?
答:其定义是特定于体系结构的但原型总是相同的,调用kernel_thread
函数可启动一个内核线程。。
问:Linux中运行的进程如何识别那些是内核线程? 答:在ps命令的输出中很容易发现其区别,其名称都置于方括号内比如:
$ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 51780 3788 ? Ss 2019 5:05 /usr/lib/systemd/systemd --system --deserialize 20
root 2 0.0 0.0 0 0 ? S 2019 0:01 [kthreadd]
问:进程运行有几种状态?
问:僵尸进程是如何产生的? 答:僵尸进程的资源已经释放但在进程表中仍存在对应的表项,其原因在于UNIX操作系统下进程创建和销毁的方式。
首先
必须由另一个进程或一个用户杀死(通常是通过发送SIGTERM或SIGKILL
信号来完成,这等价于正常地终止进程);其次
进程的父进程在子进程终止时必须调用或已经调用wait4 (读做wait for)
系统调用。这相当于向内核证实父进程已经确认子进程的终结,该系统调用使得内核可以释放为子进程保留的资源。
例如如果父进程编程极其糟糕,没有发出wait调用
),僵尸进程可能稳定地寄身于进程表中直至下一次系统重启。
问:典型的UNIX进程包括那些? 答:由二进制代码组成的应用程序、单线程(计算机沿单一路径通过代码,不会有其他路径同时运行)、分配给应用程序的一组资源(如内存、文件等)。
问:Unix多线程的实现方式? 答:有三种方式即fork 和 exec 以及 clone 方式,我们再学习Linux编程中学到的;
同一组打开文件、同样的工作目录、内存中同样的数据(两个进程各有一份副本)
等等此外二者别无关联。因为exec并不创建新进程,所以必须首先使用fork复制一个旧的程序
,然后调用exec在系统上创建另一个应用程序。新进程不是独立于父进程的
, 而可以与其共享某些资源。写时复制,直至新进程对内存页执行写操作才会复制内存页面,这比在执行fork时盲目地立即复制所有内存页要更高效。父子进程内存页之间的联系,只有对内核才是可见的,对应用程序是透明的可以指定需要共享和复制的资源种类,例如,父进程的内存数据、打开文件或安装的信号处理程序。clone用于实现线程,但仅仅该系统调用不足以做到这一点,还需要用户空间库才能提供完整的实现。Linuxthreads和Next Generation Posix Threads
等由此就可知 systcl 命令是通过 /proc/sys/ 目录下的各个接口文件实现配置的。 该目录下包含以下子目录: $ tree -L 1
├── abi ├── debug ├── dev # 设备相关信息 ├── fs # 特定的文件系统,比如 fd,inode,dentry,quota tuning ├── fscache ├── kernel # tuning 全局参数,比如 cpu 调度,printk,softirq,hung_task,numa,watchdog等 ├── net # 网络子系统相关参数,比如 ipv4,ipv6,icmp,igmp 等 ├── sunrpc ├── user └── vm # tuning 内存管理相关参数,buffer 和 cache 的管理
那么在内核中各子系统是如何导出这些参数到 procfs,并允许用户通过 echo, cat 等工具操作这些节点来设置参数的呢? 在 kernel/sysctl.c 中定义了某个子系统下的某个参数的相关 ctl_table,比如 vm.dropcaches。 先设置 vm 目录的参数,访问权限为 555,并设置 child 属性为 vm_table。 vm_table 结构体数组包含了 VM 子系统的参数,比如 dropcaches 参数,设置了该节点的访问权限为 644;data 属性值为 sysctl_drop_caches,该变量在 fs/drop_caches.c 中定义; 该节点的读写处理函数 drop_caches_sysctl_hander,在 fs/drop_caches.c 中实现,通过 dointvec_minmax 来读出数据 。 最后填充好 ctl_table 结构体后在 sysctl_init 入口函数注册这些结构体数组。 相关代码如下:
/* The default sysctl tables: */
static struct ctl_table sysctl_base_table[] = {
{
.procname = "kernel", // /proc/sys/kernel
.mode = 0555,
.child = kern_table,
},
{
.procname = "vm", // /proc/sys/vm
.mode = 0555,
.child = vm_table,
},
{
.procname = "fs", // /proc/sys/fs
.mode = 0555,
.child = fs_table,
},
{
.procname = "debug", // /proc/sys/debug
.mode = 0555,
.child = debug_table,
},
{
.procname = "dev", // /proc/sys/dev
.mode = 0555,
.child = dev_table,
},
{ }
};
static struct ctl_table vm_table[] = {
...
{
.procname = "drop_caches",
.data = &sysctl_drop_caches,
.maxlen = sizeof(int), // vm.drop_caches 变量4各字节
.mode = 0644, // /proc/sys/vm/drop_caches访问权限"644"
.proc_handler = drop_caches_sysctl_handler, // handler
.extra1 = &one,
.extra2 = &four,
},
...
};
int __init sysctl_init(void)
{
struct ctl_table_header *hdr;
// 注册 ctl_table
hdr = register_sysctl_table(sysctl_base_table);
kmemleak_not_leak(hdr);
return 0;
}
int drop_caches_sysctl_handler(struct ctl_table *table, int write,
void __user *buffer, size_t *length, loff_t *ppos)
{
int ret;
ret = proc_dointvec_minmax(table, write, buffer, length, ppos);
if (ret)
return ret;
if (write) { // 如果是写数据
static int stfu;
if (sysctl_drop_caches & 1) {
// 如果 drop_caches=1 则清 pagecache
iterate_supers(drop_pagecache_sb, NULL);
count_vm_event(DROP_PAGECACHE);
}
if (sysctl_drop_caches & 2) {
// 如果 drop_caches=2 则清 pagecache 和 slab
drop_slab();
count_vm_event(DROP_SLAB);
}
if (!stfu) {
pr_info("%s (%d): drop_caches: %d\n",
current->comm, task_pid_nr(current),
sysctl_drop_caches);
}
stfu |= sysctl_drop_caches & 4;
}
return 0;
}
通过上述分析,大致梳理了 sysctl 接口在 kernel 中运行的大致流程。
接下来,学以致用,我们可以在 /proc/sys 这个根目录下写一个 my_sysctl 的节点,首先定义并填充 ctl_table 结构体,并通过 register_sysctl_table 注册到系统。
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/sysctl.h>
static int data;
static struct ctl_table_header * my_ctl_header;
int my_sysctl_callback(
struct ctl_table *table,
int write,void __user *buffer,
size_t *lenp, loff_t *ppos)
{
int rc = proc_dointvec(
table, write, buffer, lenp, ppos);
if (write) {
printk("write operation,cur data=%d\n",
*((unsigned int*)table->data));
}
}
/* The default sysctl tables: */
static struct ctl_table my_sysctl_table[] = {
{
.procname = "my_sysctl",
.mode = 0644,
.data = &data,
.maxlen = sizeof(unsigned int),
.proc_handler = my_sysctl_callback,
},
{
},
};
static int __init sysctl_test_init(void)
{
printk("sysctl test init...\n");
my_ctl_header = register_sysctl_table(my_sysctl_table);
return 0;
}
static void __exit sysctl_test_exit(void)
{
printk("sysctl test exit...\n");
unregister_sysctl_table(my_ctl_header);
}
通过 qemu 进入目标文件系统,使用 insmod 注册驱动,在 /proc/sys 目录下出现 my_sysctl 节点,此时就可以通过 cat/echo 命令向该节点读写数据,也可以直接通过 systcl 设置该参数。
/mnt # insmod sysctl_test.ko
[ 89.904485] sysctl test init...
/mnt # sysctl my_sysctl
my_sysctl = 0
/mnt # sysctl -w my_sysctl=2
[ 151.278213] write operation,cur data=2
/mnt # sysctl my_sysctl
my_sysctl = 2
/mnt # cat /proc/sys/my_sysctl
2
四. OOM killer
OOM killer会在可用内存不足时选择性的杀掉用户进程,它的运行规则是怎样的,会选择哪些用户进程“下手”呢?OOM killer进程会为每个用户进程设置一个权值,这个权值越高,被“下手”的概率就越高,反之概率越低。每个进程的权值存放在/proc/{progress_id}/oom_score中,这个值是受/proc/{progress_id}/oom_adj的控制,oom_adj在不同的Linux版本的最小值不同,可以参考Linux源码中oom.h(从-15到-17)。当oom_adj设置为最小值时,该进程将不会被OOM killer杀掉,设置方法如下。
echo {value} > /proc/${process_id}/oom_adj
对于Redis所在的服务器来说,可以将所有Redis的oom_adj设置为最低值或者稍小的值,降低被OOM killer杀掉的概率。
for redis_pid in (pgrep -f “redis-server”)do echo -17 > /proc/
运维提示:
有关OOM killer的详细细节,可以参考Linux源码mm/oom_kill.c中oom_badness函数。
笔者认为oom_adj参数只能起到辅助作用,合理的规划内存更为重要。
通常在高可用情况下,被杀掉比僵死更好,因此不要过多依赖oom_adj配置