# 内核
# 架构
Linux 系统架构从上到下分为三层:
- User Space :运行用户态进程。
- 用户启动的进程一般运行在用户态,只访问虚拟内存空间中的用户内存空间。
- 当用户态进程调用内核 API、遇到中断或异常时,就会通过 CPU 上下文切换,下沉到内核态,有权限执行内核内存空间中的代码、访问底层硬件。
- 划分进程的用户态、内核态是为了提高系统的安全性、稳定性,避免进程随便调用内核 API 导致内核故障。
- Kernel Space :运行内核态进程。
- System Call Interface(SCI):向上对用户空间提供内核接口。
- Virtual File System(VFS):一个抽象的文件系统层,向下管理不同类型的文件系统,向上提供统一的文件系统接口。
- File System :实际的文件系统层,包括多种文件系统。
- General Block Device Layer :一个抽象的块设备层,向下管理不同类型的硬件设备,向上提供统一的 IO 接口。
- Device Driver :有很多种,分别管理不同设备。
- Hardware Layer :计算机底层的硬件层。
# 中断
Linux 系统处理中断的过程通常分为两步:
- 处理硬件中断请求,立即处理其中不能延误的任务。
- 以软件中断的形式执行剩下的任务,比如创建成内核线程。
- 这样既可以及时执行中断中重要的任务,又可以延迟执行耗时久的任务。
- 例如:当网卡收到数据帧时,会发出一个硬中断,让网卡驱动程序将数据帧拷贝到内核缓冲区中。然后网卡驱动程序发出一个软中断,通知某个内核进程来解析数据帧,转换成 IP、TCP 等协议包,最后交给给应用层的进程。
# 内核线程
Linux 启动时,kthreadd 进程会创建一些内核线程(kernel thread),用于执行一些内核任务。
- 内核线程是一种特殊的进程,有 task_struct 结构体,有独立的 PID 。但只能在内核态运行,没有虚拟内存空间。
内核线程举例:
- kthreadd
- 系统开机时创建的第三个进程,PID 为 2 ,负责创建、管理所有内核线程。
- kworker
- 负责并行执行内核 Workqueue 中的任务,比如定时器、中断、IO 。
- 在 CPU 每个核上运行一个实例。
- migration
- 负责在多个 CPU 核之间迁移运行中的进程,从而避免单核故障、实现负载均衡。
- 在 CPU 每个核上运行一个实例。
- watchdog
- 负责避免系统死机。进程启动之后会打开 /dev/watchdog 文件,不断对它进行写操作。如果在一定时间内(默认为 1 分钟)没有进行写操作,就会通过硬件或软件重启系统。
- 在 CPU 每个核上运行一个实例。
- kauditd
- 负责记录各种系统调用、文件访问的审计日志,保存到 /var/log/audit/* 。
- kblockd
- 负责管理块设备。
- 在系统中只运行一个实例。
- kswapd0
- 负责管理 Swap 分区。
- kthreadd
# 内核参数
# sysctl
:一个内置命令,用于读取、配置系统运行时的内核参数。
- 用法:
sysctl <var> # 读取指定参数的值 [-w] <var>=<value> # 设置指定参数的值 -a # 显示所有可修改的内核参数 -p <file> # 加载指定文件中的配置参数(默认是 /etc/sysctl.conf 文件)
- 执行
sysctl -a
会从/proc/sys/
目录下的各个文件读取内核参数(超过一千个),然后打印到终端。- 例如,参数
net.ipv4.tcp_keepalive_time
对应的文件是/proc/sys/net/ipv4/tcp_keepalive_time
。
- 例如,参数
- 用 sysctl 命令可修改运行时参数,但是在主机重启时会复原。
- 将配置参数保存到
/etc/sysctl.conf
文件中才能永久生效。系统每次重启时会自动加载该文件,用户也可以执行sysctl -p
主动加载。
- 将配置参数保存到
- 执行
# 配置参数
参考文档:
配置文件采用 INI 格式。
- 键值对用 = 分隔,中间的空格会被忽略。
- 值通常是字符串型、整型或布尔型。
- 其中,布尔型用 1、0 分别表示 True、False 。
关于进程:
kernel.pid_max = 32768 # 整个系统中,同时运行进程的最大数量 kernel.threads-max = 126935 # 整个系统中,同时运行线程的最大数量。默认值与主机内存成正比
关于内存:
vm.max_map_count = 65530 # 每个进程创建的内存映射区域(memory map areas)的最大数量,它们用于 malloc、shared librarie vm.swappiness = 30 # 使用 swap 内存的推荐度,取值范围为 0~100 ,0 表示禁用 vm.overcommit_memory = 0 # 是否允许内核分配出去的内存超过物理内存的总量,因为分配的内存不一定会被进程实际使用 # 0 :根据启发式算法适量允许 # 1 :始终允许,此时可能因为物理内存不足而触发 OOM # 2 :始终不允许超过物理内存的 vm.overcommit_ratio 百分比,此时不会触发 OOM ,但可能因为内存不足而不能创建新进程 vm.overcommit_ratio = 50 vm.oom_kill_allocating_task = 0 # 执行 OOM 的策略。取值如下: # 0 :扫描全部进程,选出 oom_score 最大的进程然后杀死 # 1 :杀死当前申请内存而导致内存不足的进程 vm.panic_on_oom = 0 # 是否不执行 OOM ,而是让系统重启
关于文件:
fs.file-max = 791098 # 整个主机上,允许同时打开文件的最大数量 # 每个文件的 inode、dcache 大概占用 1k 内存。默认设置的 fs.file-max 会允许文件占用可用内存的 10%
关于网络:
net.ipv4.icmp_echo_ignore_all = 0 # 是否忽略所有 icmp 包,这样本机就不能被 ping 到 net.ipv4.icmp_echo_ignore_broadcasts = 1 # 是否忽略发向广播地址的 icmp 包 net.ipv4.ip_forward = 1 # 如果一个网口收到数据包时,不匹配目标 IP ,是否转发给其它匹配目标 IP 的网口,相当于路由转发功能。建议设置为 1 net.ipv4.ip_local_port_range = 10000 65535 # 允许 Socket 绑定的本地端口范围。默认值为 10000 65535 或 32768 60999 # Linux 会自动调整每个 Socket 的接收缓冲区、发送缓冲区占用的内存容量 net.ipv4.tcp_moderate_rcvbuf = 1 # 是否自动调整每个 Socket 接收缓冲区的容量 net.core.rmem_default = 212992 # 每个 Socket 接收缓冲区的默认容量,默认为 208 KB net.core.wmem_default = 212992 # 每个 Socket 发送缓冲区的最大容量 net.core.rmem_max = 212992 # 最大容量 net.core.wmem_max = 212992 net.ipv4.tcp_rmem = 4096 87380 6291456 # 每个 TCP Socket 接收缓冲区的容量,分别指定最小值、默认值、最大值,单位 bytes 。默认为 4KB、86KB、6MB net.ipv4.tcp_wmem = 4096 16384 4194304 # 每个 TCP Socket 发送缓冲区的容量。默认为 4KB、16KB、4MB net.ipv4.tcp_mem = 88500 118001 177000 # 整个主机上,全部 TCP Socket 缓冲区的容量,分别指定最小值、压力值、最大值,单位为内存页 pages 。默认值与主机内存成正比 # 全部容量超过压力值时,Linux 开始节省给 Socket 分配的内存。此时每个 TCP Socket 缓冲区的容量可能降低,但不会低于每个的最小值 # 全部容量超过最大值时,Linux 禁止给 Socket 分配新的内存。此时不能建立新的 TCP 连接 net.ipv4.udp_mem = 574224 765634 1148448 net.ipv4.udp_rmem_min = 4096 net.ipv4.udp_wmem_min = 4096
关于 ARP :
# 默认配置 net.ipv4.neigh.default.base_reachable_time_ms = 30000 # 每条 ARP 记录大概缓存多少毫秒。如果一个 ARP 记录长时间未被使用,则从 reachable 状态改为 stale 状态 net.ipv4.neigh.default.gc_stale_time = 60 # 每隔多久,将长时间未使用的 ARP 记录改为 stale 状态。此时 stale 记录尚未删除,但已失效,访问其 IP 时需要重新通过 ARP 协议查询 Mac 地址 net.ipv4.neigh.default.gc_interval = 30 # 每隔多久 GC 一次,删除 stale 状态的 ARP 记录 net.ipv4.neigh.default.gc_thresh1 = 128 # 至少缓存多少条 ARP 记录时,才开始 GC net.ipv4.neigh.default.gc_thresh3 = 1024 # 最多缓存多少条 ARP 记录 # 每个网口有一份配置,继承默认配置 net.ipv4.neigh.eth0.gc_interval = 30
关于 TCP 通信:
# 建立 TCP 连接 net.core.somaxconn = 128 # 限制了调用 listen() 函数时,backlog 的最大值 net.ipv4.tcp_max_syn_backlog = 128 # SYN 队列的最大长度,默认值与内存成正比 net.ipv4.tcp_syncookies = 1 # 是否启用 SYN Cookies 功能。取值如下: # 0 :不启用 # 1 :默认值,当 SYN 队列满了时启用 # 2 :总是启用 net.ipv4.tcp_abort_on_overflow= 0 # 当 accept 队列满了时,server 怎么处理新的 ESTABLISHED 连接。取值如下: # 0 :默认值,丢弃 client 发来的 ACK 包 # 1 :回复一个 RST 包给 client ,表示拒绝连接 # 关闭 TCP 连接 net.ipv4.tcp_fin_timeout = 60 # 如果 Socket 保持在 FIN-WAIT-2 状态超过该时长,则关闭 Socket 。通常设置为 2*MSL 时长 net.ipv4.tcp_max_orphans = 65536 # 限制 orphan Socket 的最大数量,超过限制则自动回收。默认值与主机内存成正比 net.ipv4.tcp_max_tw_buckets = 65536 # 限制 TIME-WAIT 状态的 Socket 的数量,超过限制则会立即关闭 Socket 。默认值与内存成正比 # 一些 TCP 配置参数 net.ipv4.tcp_window_scaling = 1 # 是否启用 Window scale 功能,默认启用 net.ipv4.tcp_congestion_control = cubic # 采用哪种 TCP 拥塞控制算法 net.ipv4.tcp_slow_start_after_idle = 1 # 是否启用慢启动重启(Slow-Start Restart,SSR),当 TCP 连接空闲一段时间之后,重置拥塞窗口 cwnd ,重新慢启动 # TCP 长连接: # 如果本机的一个 TCP 连接长达 tcp_keepalive_time 秒没有用于数据通信,则探测一下对方主机是否仍然在线。每隔 tcp_keepalive_intvl 秒探测一次,最多探测 tcp_keepalive_probes 次 # 因此一个 TCP 连接空闲时长达到 tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes 之后会被关闭 # 减小这些参数,可以尽早关闭无用的 Socket ,比如 CLOSE_WAIT 状态的 Socket net.ipv4.tcp_keepalive_time = 7200 net.ipv4.tcp_keepalive_intvl = 75 net.ipv4.tcp_keepalive_probes = 9 # 复用 TCP 连接:如果同一对 client、server 多次建立 TCP 连接,旧连接的 Socket 处于 TIME-WAIT 状态而尚未关闭,则建立新连接时,是否允许复用旧 Socket ,而不是创建新 Socket net.ipv4.tcp_tw_reuse = 0 # 当 client 多次向同一个 server 建立 TCP 连接时,是否允许复用 TIME-WAIT 状态的 Socket 。该参数只能作用于 client 即主动连接方,对 server 无效 net.ipv4.tcp_tw_recycle = 0 # 当 server 多次被同一个 client 建立 TCP 连接时,是否允许复用 TIME-WAIT 状态的 Socket 。该行为可能搞混在同一 NAT 网关之后的不同 client ,因此从 Linux 4.12 开始弃用 # TFO(TCP Fast Open):Linux 3.7 开始提供的一个功能,能将 TCP 三次握手缩短一个 RTT 耗时,但存在安全隐患,因此默认禁用 # 原理:如果同一对 client、server 多次建立 TCP 连接,则 client 可在本机记录 TFO cookie ,等到重新建立连接时,在握手的第一步发送 SYN 包,并在 TCP Options 中包含 cookie 来证明自己的身份。使得 server 在握手的第二步之后,就可以开始向 client 传输数据,然后依然进行握手的第三步 # client、server 需要同时启用 TFO 功能,才能生效 net.ipv4.tcp_fastopen = 0
关于 netfilter :
net.netfilter.nf_conntrack_max = 65536 # nf_conntrack 表中最多记录多少个 TCP 连接,默认值与主机 CPU 核数、内存成正比 net.netfilter.nf_conntrack_buckets = 16384 # nf_conntrack 表中的 bucket 数量,需要设置为 conntrack_max / bucket_size # nf_conntrack 是一个哈希表,由大量 bucket 组成。每个 bucket 是一个链表结构,记录 bucket_size 条 TCP 连接。每条记录大概占用 320 bytes 的内核内存 # bucket_size 取决于 `conntrack_max / conntrack_buckets` ,在 32、64 位系统分别建议为 4、8 。 # 执行 echo 16384 > /sys/module/nf_conntrack/parameters/hashsize 可在线设置该参数,但重启主机时会复原 # 执行 echo 'options nf_conntrack hashsize=16384' >> /etc/modprobe.d/iptables.conf 可永久设置该参数,但需要重启主机 net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 60 net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 120 net.netfilter.nf_conntrack_tcp_timeout_established = 432000 # 如果一个 ESTABLISHED 状态的 TCP 连接,保持空闲超过该时长,则删除该记录。默认为 432000 秒即 5 天,可改为 1 小时 net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120 # 默认为 120 秒,对应 2*MSL ,可改为 10 秒 net.netfilter.nf_conntrack_tcp_timeout_close_wait = 60 net.netfilter.nf_conntrack_udp_timeout = 30 net.netfilter.nf_conntrack_icmp_timeout = 30
如果服务器被大量客户端并发访问(比如每秒访问上百次、上千次),则可能遇到性能问题,比如响应耗时增加、丢包率增加。解决方案如下:
- 首先,监控服务器的 CPU、内存、磁盘、带宽等资源的使用率。如果某一种接近 100% ,则可能是这种资源遇到了性能瓶颈。
- 然后,考虑降低上述资源的使用率,比如:
- 花钱扩容服务器资源。
- 优化服务器、客户端的程序代码、配置参数,减少占用的服务器资源。
- 优化服务器的内核参数,支持更多并发 TCP 连接:
- 增加 ulimit -n 参数,允许同时打开大量 Socket 文件。
- nf_conntrack 记录满了时会拒绝新的 TCP 连接,因此需要增加 nf_conntrack_max 、减小 nf_conntrack_xx_timeout_xx 。
- 频繁建立新的 TCP 连接时,建议调小 net.ipv4.tcp_keepalive_time ,更早关闭空闲连接。
- 并发连接过多时,需要增加 net.ipv4.tcp_mem 的最大值、增加主机内存。因为每个 Socket 的接收缓冲区加发送缓冲区,可能占用 4KB~10MB 内存。
- 每个 TCP Socket 默认的 tcp_rmem、tcp_wmem 一般足够使用,不需要修改,因为 RDP 不会很大。
# ulimit
:一个内置命令,用于设置 shell 终端占用的系统资源上限。
命令:
$ ulimit -a # 显示各种资源的限制信息 -H # 选择硬限制 -S # 选择软限制 -u # 显示 -u 参数的值 -u <n> # nproc ,限制同时运行的进程数最多为 n 个(不能限制 root 用户) -m <n> # rss ,限制占用的常驻内存最多为 n kb(大部分 Linux 发行版不支持该限制) -v <n> # 限制占用的虚拟内存最多为 n kb -s <n> # stack ,限制堆栈的大小为 n kb -t <n> # cpu ,限制每个进程累计占用的 CPU 时长,超过限制时则发送 SIGKILL 信号终止该进程。进程的运行状态为 Sleeping 时不计入时长 -n <n> # nofile ,限制同时打开的文件描述符数量最多为 n -f <n> # fsize ,限制创建每个文件的大小最大为 n blocks
例:
ulimit -a # 显示限制(默认是软限制) ulimit -aH # 显示硬限制 ulimit -nH unlimited # 设置硬限制 ulimit -nS 1024 # 设置软限制 ulimit -n unlimited # 同时设置硬限制和软限制
- 当进程占用的资源超过限制时,会被立即杀死。
- 软限制的值不能超过硬限制。
- 非 root 用户无权调大硬限制,只能调小硬限制、改变软限制。
ulimit 的配置只能作用于当前 shell 终端。而
/etc/security/limits.conf
文件中的配置会对指定用户永久生效,如下:* hard nofile unlimited * soft nofile 1024 * hard maxlogins 3 * soft maxlogins 3
每行的格式如下:
<domain> <type> <item> <value>
- domain :用户名或组名,也可用 * 匹配所有。
- type :取值为 hard 或 soft ,表示硬限制或软限制。
- item、value :限制项及其值。比如:
- maxlogins :当前用户同时登录的最大数量
- maxsyslogins :系统同时登录的最大数量
# eBPF
Linux 的内存分为内核空间、用户空间两部分,前者只允许内核态进程使用。想在内核空间执行自定义程序时,有几种方案:
- LKM(Linux Kernel Module,Linux内核模块)
- 原理:用户用 C 语言开发一个 Linux 内核模块,编译成扩展名为 .ko 的文件,然后用 insmod 命令加载到内核中。
- 执行命令
lsmod
查看已加载的所有内核模块。
- 执行命令
- 优点:可以在 Linux 内核运行时,加载新模块,而无需重新编译内核。
- 缺点:模块运行出错时,可能导致 Linux 内核崩溃。
- 原理:用户用 C 语言开发一个 Linux 内核模块,编译成扩展名为 .ko 的文件,然后用 insmod 命令加载到内核中。
- eBPF(extended Berkeley Packet Filter,扩展伯克利包过滤器)
- 2014 年,Linux kernel 3.15 添加了 eBPF 功能。
- 原理:用户用 C 语言开发一个 eBPF 程序,用 BCC(BPF编译器集合)编译成字节码文件,然后加载到内核中,采用 JIT 技术即时编译成机器码。
- 当 Linux 内核发生系统调用、网络包等 event 时,会通过 hook 机制自动执行 eBPF 程序。
- 执行命令
bpftool prog list
查看已加载的所有 eBPF 程序。
- 常见用途:
- 用于网络包过滤,比用户态进程的速度更快、吞吐量更高。
- 用于监控,在内核中收集、生成监控指标。
- 用于 debug ,跟踪某些程序的运行。
- 优点:
- 允许内核执行自定义代码,而无需重新编译内核,或加载新模块。
- 加载 eBPF 程序到内核时,会进行多种检查,比如确保该程序没有死循环,不会导致内核崩溃。