2 - Hand on system programming with Linux - 资源限制
Resource Limits 资源限制
在本章中,我们会查看单个进程的资源限制,这些限制是什么,为什么需要这些限制。
资源限制
网络中一种常见的攻击手段是分布式拒绝服务攻击 DDos: Distributed denial of service attack
,攻击者企图消耗目标系统的资源,令目标系统崩溃,或者至少令目标系统不再能够正常响应。
在一个未经优化的系统上,执行这类攻击是十分简单的,比如,假设我们可以通过普通用户身份通过 shell 访问一台服务器,我们可以使用 dd
命令耗费它的硬盘空间,dd
命令的一个用法是创建任意长度的文件。
$ dd if=/dev/urandom of=tst count=1024 bs=1M
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 15.2602 s, 70.4 MB/s
$ ls -lh tst
-rw-rw-r-- 1 kai kai 1.0G Jan 4 12:19 tst
如果我们将块大小修改为 1G,比如:
dd if=/dev/urandom of=tst count=1024 bs=1G
dd
将会尝试创建 1024 GB 大小的文件,如果我们以循环的方式执行这个命令呢!
为了控制资源使用,类 UNIX 系统都具有资源限制,一个由操作系统提供的由使用者设置的资源限制。在进行介绍之前,需要声明,这些资源限制是对每一个进程的,并不针对系统全局。在进行更细致的介绍之前,我们回到吃掉系统盘空间的攻击手段,只是这次系统设置了资源限制。
查看并设置资源限制可以由 shell 命令 ulimit
实现,使用 -f
选项询问最大文件限制:
$ ulimit -f
unlimited
这里显式无限制,当然实际并非如此,这里只是说明操作系统并没有做出对应的限制,但是实际的物理可用盘空间限制了实际的应用。
让我们设置一个文件大小限制,只需要向 -f
选项传递一个参数,可以查看 ulimit
的 man 帮助页面查看具体的使用方式。当我们设置 ulimit -f 2
时,实际设置的文件大小为 2048 字节的限制,我们再次使用 dd
测试:
$ ulimit -f
unlimited
$ ulimit -f 2
$ ulimit -f
2
$ dd if=/dev/urandom of=tst count=2048 bs=1
2048+0 records in
2048+0 records out
2048 bytes (2.0 KB, 2.0 KiB) copied, 0.00688134 s, 298 kB/s
$ dd if=/dev/urandom of=tst count=2049 bs=1
File size limit exceeded (core dumped)
资源限制范围
在前面的 dd
例子中,我们可以设置最大文件大小,那么问题是,这个资源限制的范围是什么,这个限制是否是整个系统的限制?答案是简单的:不是,它并不是全系统有效的,而只是进程级别的。考虑有两个 shell,它们是两个 bash 进程,shell A 与 shell B。我们在 shell A 中进行了文件大小的资源限制配置,但是没有在 shell B 中执行对应的资源限制命令。我们按照上一章中的命令执行,会发现在 shell A 中会有资源限制的失败提示,但在 shell B 中只要资源够用,没有这样的失败提示。
资源类型
可用资源限制
资源限制 | ulimit 选项 | 默认值 | 单位 |
---|---|---|---|
最大核心文件大小 | -c | 无限制 | KB |
最大数据段大小 | -d | 无限制 | KB |
最大调度优先级(nice) | -e | 0 | |
最大文件大小 | -f | 无限制 | KB |
最大待处理信号 | -i | 可变 | |
最大锁定内存 | -l | 可变 | KB |
最大内存大小 | -m | 无限制 | KB |
最多打开文件 | -n | 1024 | |
最大管道大小 | -p | 8 | 512 字节 |
最大 POSIX 消息队列 | -q | 可变 | |
最大实时调度优先级 | -r | 0 | |
最大栈段大小 | -s | 8192 | KB |
最大 CPU 时间 | -t | 无限制 | 秒 |
最多用户进程 | -u | 可变 | |
地址空间限制/最大虚拟内存 | -v | 无限制 | KB |
最多文件锁定持有 | -x | 无限制 |
上面表格中的第三列信息中的可变,表示实际的值是基于系统资源的,比如 RAM 大小等。
使用 ulimit -a
可以列出所有的限制项。需要注意的是,这些限制都是限制在本 bash 进程的。
软/硬限制
类 UNIX 系统做了更深度的区分,资源限制有两类:
- 硬限制值
- 软限制值
硬限制是真实的最大值,使用时不能超过这个值,如果进程企图超过这个限制,将会被操作系统杀死。而软限制,处理方式则不同,当发生超限情况时,内核会向进程发送一个信号,我们可以将这个信号视作是一种警告,比如当进程企图超过软限制的文件大小,操作系统将会向它发送 SIGXFSZ: SIGnal: eXceeding FileSize
信号,如果进程企图超过 CPU 资源软限制,那么它将会收到 SIGXCPU
信号,当操作系统向进程发送多次 SIGXCPU
后,操作系统将会向它发送最后通牒 SIGKILL
杀死这个进程。
我们可以将硬限制视作是装着软限制的小盒子,软限制的范围就是 (0 - 应限制)。如果希望同时查看 shell 进程的软限制与硬限制,可以使用 -S
选项查看软限制以及 -H
选项查看硬限制,下面是我的 Ubuntu 的软资源限制:
$ ulimit -aS
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 126981
max locked memory (kbytes, -l) 65536
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 126981
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
在典型 Linux 系统上,硬限制与软限制只有两处区别:最多打开的文件数量,最大栈大小。
询问并修改资源限制值
我们现在理解了,是操作系统内核设置了每一个进程的资源限制,并追踪进程的资源使用情况,并在必要时杀死进程。
任何人都可以询问进程拥有的硬软限制。但是在设置时,对于硬限制,一旦设定该会话下不能被增加,而软限制则只能设置在 0 到硬限制之间,当使用 ulimit
设置资源限制时,系统内部会同时设定硬限制与软限制。
设置资源限制的权限给定如下:
-
具有特权的进程(比如 superuser、root、sysadmin 或者其他具有 CAP_SYS_RESOURCE 位的用户),可以同时修改硬限制与软限制
-
普通用户
- 软限制可以在 0 到硬限制之间调整
- 硬限制则只能降低,但是一旦降低就不能再增加
但凡事都有特例,一名普通用户可以修改 core file 的资源限制,这通常被开发者用来生成 core dump。
警告
设置资源限制到较高的值,需要特权。比如我们希望修改打开文件的限制,将其从 1000 修改到 2000,我们可能会想使用 sudo
来完成这个工作,但是执行 sudo ulimit -n 2000
并不能完成我们的任务,这是因为 sudo
期望 ulimit
是一个二进制可执行文件,因此会在 PATH
中搜索这个工具,但是 ulimit
实际上是 shell 内建命令,因此并不能被搜索到,需要按照下面这种方式执行:
$ ulimit -n
1024
$ sudo bash -c "ulimit -n 2000 && exec ulimit -n"
[sudo] password for arv:
2000
需要注意的是,我们不能使用 ulimit
修改 pipe 资源限制,这个资源限制是在 /proc/sys/fs/pipe-max-size
中规定的,如果希望修改这一限制,需要使用 fcntl(2)
系统调用,通过 F_GETPIPE_SZ
与 F_SETPIPE_SZ
参数修改。
prlimit 工具
除了使用 ulimit
,另一个查询展示资源限制的工具是 prlimit
,prlimit
与 ulimit
主要有下面几点不同:
- 它是更新的工具,支持 2.6.36 以上 Linux 内核版本
- 它可以按需修改资源限制,并载入另一个程序将会继承这个限制
- 这是一个二进制可执行工具,而不是像
ulimi
那样是 shell 内建命令
如果不给参数,prilimit
将会展示它自己的所有资源限制,用户可以传递 <name = value> 参数对来设置查询的资源,给定进程的 PID 来指定要查询的进程资源限制,或给定一个要加载的命令,设置新的资源限制:
prlimit [options] [--resource[=limits]] [--pid PID]
prlimit [options] [--resource[=limits]] command [argument...]
prlimit 用例
用例 1,查询限制:
$ prlimit
$ prlimit --pid=2917
用例 2,设置进程最大文件与栈大小:
$ prlimit --pid=2917 --fsize=2048000 --stack=12582912
用例 3,以给定的资源限制执行一个程序,比如 rlimit_primes
(程序源码),程序用来生成质数,我们可以让它在规定 CPU 时间内生成大的质数:
$ ./rlimit_primes
Usage: ./rlimit_primes limit-to-generate-primes-upto CPU-time-
limit
arg1 : max is 10000000
arg2 : CPU-time-limit:
-2 = don't set
-1 = unlimited
0 = 1s
$ prlimit --cpu=2 ./rlimit_primes 8000000 -2
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53,
59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113,
127, 131,
[...]
18353, 18367, 18371, 18379, 18397, 18401, 18413, 18427,
18433, 18439,
18443, 18451, 18457, 18461, 18481, 18493, 18503, 18517,
18521, 18523,
18539, 18541, 18553, 18583, 18587, 18593,
Killed
可以看到,当它消耗的资源超过了给它设定的 CPU 时间资源限制,它被内核杀死。上面 Killed
打印表示进程已经被操作系统杀死。
在 2.6.24 以上的内核版本,我们查看 /proc/
API 接口
查询、设置资源限制的编程,可以通过如下系统调用接口实现:
- getrlimit
- setrlimit
- prlimit
这里我们仅关注 prlimit(2)
接口,为了令 prlimit(2)
能够正常工作,使用的 Linux 版本需要高于 2.6.36。可以使用 uname
命令查看自己的内核版本。
我们可以查看下 prlimit
系统调用接口:
#include <sys/resource.h>
int prlimit(pid_t pid, int resource,
const struct rlimit *new_limit, struct rlimit *old_limit);
prlimit
系统调用可以用来查询、设置资源限制,每次调用只能够修改一个进程的一个资源限制。它接收四个参数,第一个参数为进程的 PID,如果参数为 0,那么这次调用是针对本进程的;第二个参数是我们要查询或设置的限制资源的名称;第三个参数与第四个参数都是 struct rlimit
指针,第三个参数如果非空,是我们要设置的新值,第四个参数如果非空,是用来接收当前限制的空间。
rlimit
结构体包含两个成员,分别表示软硬限制:
struct rlimit {
rlimit_t rlim_cur; /* Soft limit */
rlimit_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
};
prlimit
的第二个参数,可以用下面的枚举表表示:
资源限制 | 枚举名称 | 默认值 | 单位 |
---|---|---|---|
最大核心文件大小 | RLIMIT_CORE | unlimited | KB |
最大数据段大小 | RLIMIT_DATA | unlimited | KB |
最大可调度优先级 | RLIMIT_NICE | 0 | |
最大文件大小 | RLIMIT_FSIZE | unlimited | KB |
最大代处理信号 | RLIMIT_SIGPENDING | 可变 | |
最大锁定内存 | RLIMIT_MEMLOCK | 可变 | KB |
最多打开文件 | RLIMIT_NOFILE | 1024 | |
最大 POSIX 消息队列 | RLIMIT_MSGQUEUE | 可变 | |
最大实时调度优先级 | RLIMIT_RTTIME | 0 | 微妙 |
最大栈段大小 | RLIMIT_STACK | 8192 | KB |
最大 CPU 时间 | RLIMIT_CPU | unlimited | 秒 |
最大用户进程 | RLIMIT_NPROC | 可变 | |
地址空间限制或最大虚拟内存 | RLIMIT_AS | unlimited | KB |
最多文件锁定持有 | RLIMIT_LOCKS | unlimited |