Linux 资源限制

2017-05-09 linux

如何通过最简单的设置来实现最有效的性能调优,在有限资源的条件下保证程序的运作,ulimit 是在处理这些问题时,经常使用的一种简单手段。

ulimit 是一种 Linux 系统的内键功能,它具有一套参数集,用于为由它生成的 shell 进程及其子进程的资源使用设置限制。

Fork 炸弹

简单来说,这是一个恶意程序,它的内部是一个不断在 fork 进程的无限递归循环,不需要有特别的权限即可对系统造成破坏。

Jaromil 在 2002 年通过 Bash 设计了最为精简的一个 fork 炸弹,也就是 :() { :|:& };: 或者 .() { .|.& };.

这行命令如果这样写成如下的多行 Bash Script 就不难理解了:

:()
{
:|: &
}
;
:
  • 第 1 行说明下面要定义一个函数,函数名为小数点或者冒号,没有可选参数。
  • 第 2 行表示函数体开始。
  • 第 3 行是函数体真正要做的事情,首先它递归调用本函数,然后利用管道调用一个新进程(它要做的事情也是递归调用本函数),并将其放到后台执行。
  • 第 4 行表示函数体结束。
  • 第 5 行并不会执行什么操作,在命令行中用来分隔两个命令用。从总体来看,它表明这段程序包含两个部分,首先定义了一个函数,然后调用这个函数。
  • 第 6 行表示调用本函数。

冒号或者逗号其实是函数名,这个脚本就是在不断的执行该函数,然后不断 fork 出新的进程。

那么,有没有办法扼制这种情况的发生呢?答案是肯定的,只需设置进程的 limit 数即可。

在上面的例子中,我们将用户可以创建的最大进程数限制为 128,执行fork炸弹会迅速 fork 出大量进程,此后会由于资源不足而无法继续执行。使用工具ulimit即可设置各种限制数,具体的请参考该工具的man或help。

Linux 资源限制

一般最常使用的是文件句柄和进程数,这里简单通过这两个介绍。

其中设置时使用最多的是 limits.confsysctl.conf 区别在于,前者只针对用户,而后者是针对整个系统参数配置的。

默认的文件句柄限制是 1024,可以通过 ulimit -n 查看;系统总的限制保存在 /proc/sys/fs/file-max 文件中,可以通过 /etc/sysctl.conf 配置文件修改,当前整个系统的文件句柄使用数可以查看 /proc/sys/fs/file-nr

注意,在改变资源限制的时候需要保证如下的几个准则:

  • 进程的软限制需要小于等于硬限制;
  • 普通用户只能缩小硬限制,而且不可逆;只有超级用户可以扩大限制。

也就是说,通过硬限制来控制用户的软限制,而通过软限制控制用户对资源的使用。其中可以配置的资源选项可以通过 man 3 prlimit 查看。

注意,Linux 中的 ulimit 命令只对当前会话有效,已启动进程没法用这个命令修改限制,在 2.6.36 内核版本之后,新增了 prlimit API 用于动态修改某个进程的 rlimits ,也可以直接使用 prlimit 命令进行修改。

ulimit

该命令是 bash 内键命令,可以通过 type ulimit 查看,它具有一套参数集,用来为由它生成的 shell 进程及其子进程的资源使用设置限制,针对的是 Per-Process 而非 Per-User 。

ulimit 用于 shell 启动进程所占用的资源,可以用来设置系统的限制,通过 ulimit -a 可以查看当前的资源限制,如果通过命令行设置,则只对当前的终端生效。

永久生效

一般有两种方法:

  • 命令写到 profile 或 bashrc 中,也即登陆时自动修改限制,如 ulimit -S -c 0 > /dev/null 2>&1
  • /etc/security/limits.conf 文件中添加记录,并且在 /etc/pam.d/ 中的 seesion 有使用到 limit 模块,注意需要重启生效。

一般配置文件中的格式如下:

domain type item value
   domain 指定用户名或者通过@指定用户组,其中*表示所有用户
   type 也就是软设置或者硬设置,也就是hard或者soft,通过-标示同时设置两个值;
   item 指定想限制的资源,例如cpu nproc maxlogins等;
   value 相关指标对应的值,其中 unlimited 表示不限制。

Hard VS. Soft

其中的硬限制是实际的限制,而软限制,是 Warnning 限制,只会做出 Warning;在通过 ulimit 设置时分软硬设置,加 -H 就是硬,加 -S 就是软,默认是 -S

如果打开文件过多,会导致 Too many open files 报错,

ulimit limits.conf pam_limits

在 Linux 中,每个进程都可以调用 getrlimit() 来查看自己的 limits,也可以调用 setrlimit() 来改变自身的 soft limits,如果要修改 hard limit,则需要确保进程有 CAP_SYS_RESOURCE 权限。

另外,进程 fork() 出来的子进程,会继承父进程的 limits 设定。

ulimit 是 shell 的内置命令,同样是调用上述的接口获取改变自身的 limits ,当在 shell 中执行应用程序时,相应的进程就会继承当前 shell 的 limits 设置。

而 shell 的初始 limits 是在启动时通过 pam_limits 设定的,这是一个 PAM 模块,用户登录会根据 limits.conf 定义的值进行配置。

可以开启 pam_limits 的 debug 来查看大致过程:

$ cat /etc/security/limits.conf
$ grep pam_limits /etc/pam.d/password-auth-ac
session     required      pam_limits.so debug
$ tail /var/log/secure

也即是说:

  1. 用户进行登录时触发 pam_limits 模块;
  2. pam_limits 会读取 limits.conf 中的配置,并设定用户所登陆 shell 的 limits;
  3. 用户登陆 shell 之后,可以通过 ulimit 命令查看或者修改当前 shell 的 limits;
  4. 当用户在 shell 中执行程序时,该程序进程会继承 shell 的 limits 值,从而使子进程相关的配置生效。

关于PAM

在通过第二种方式配置永久生效时,可以查看 /etc/pam.d/login 文件,确保有 session required /lib/security/pam_limits.so 配置项。

简单来说,limits.conf 文件实际是 Linux PAM 中 pam_limits.so 的配置文件。

例如,限制 admin 用户登录到 sshd 的服务不能超过 2 个。

----- 在/etc/pam.d/sshd中添加
session required pam_limits.so
----- 在/etc/security/limits.conf中添加
admin - maxlogins 2

如果要查看应用程序知否支持 PAM ,最简答的是通过 ldd 查看动态库。

查看进程的限制

进程自己可以通过 getrlimit() prlimit() 来获得当前 limits 配置,调用 getrusage() 获取自身的资源使用量,也可以通过 /proc/PID/limits 获取某个进程的 limits 设置。

要查看某个进程的资源使用量,通常可以通过 /proc/PID/{stat,status} 查看。具体某个值的含义,可以参考 proc 的手册。

资源限制方法

如下是在 CentOS 上系统的默认配置。

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0             # 优先级限定
file size               (blocks, -f) unlimited
pending signals                 (-i) 31136         # 最大Pending的信号数
max locked memory       (kbytes, -l) 64            # 内存锁定
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024          # 最大打开文件
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200        # POSIX消息队列的最大值
real-time priority              (-r) 0             # 实时调度的优先级设置
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited     # CPU实际使用时间
max user processes              (-u) 4096
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

C-API

#include <sys/time.h>
#include <sys/resource.h>

int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);

int prlimit(pid_t pid, int resource, const struct rlimit *new_limit,
                   struct rlimit *old_limit);

在内核中,其暴露的接口包括 prlimit() setrlimit() getrlimit() ,详细可以通过 man 3 prlimit 查看,其中 struct rlimit 对应的结构如下:

struct rlimit {
	rlim_t rlim_cur;  /* Soft limit */
	rlim_t rlim_max;  /* Hard limit (ceiling for rlim_cur) */
};

实际上最后都会调用内核中的 do_prlimit() 函数。

为了方便测试,同时提供了一个测试程序。

优先级限定

Scheduling Priority 也就是进程的 nice 值设置,注意,只对普通用户其作用,超级用户由于拥有 CAP_SYS_NICE 权限导致控制无效。

----- 硬限制设置为-15~20,并查看
# ulimit -H -e 35
# ulimit -He

----- 软限制设置为-10~20,并查看
# ulimit -S -e 30
# ulimit -Se
# nice -n -11 ls /tmp

----- 切换到非root用户并执行ls命令
# su foobar
$ nice -n -10 ls /tmp
$ nice -n -11 ls /tmp
nice: cannot set niceness: Permission denied

如上,当超过了软限制之后会直接报 Permission denied 错误,而 root 用户实际上不会被限制的。

内存锁定值限定

Max Locked Memory 同样由于 CAP_IPC_LOCK 参数,实际上不会限制 root 用户的使用。

由于 Linux 采用的是虚拟内存,通常虚拟地址要远大于实际的物理内存,有时物理内存的数据会被换到交换区中,而有时交换区的内存会换到物理内存中。

常见的场景有:

  1. 出于性能考虑,例如数据库通常会将数据锁定到物理内存中。
  2. 安全需要,比如用户名、密码等内容被交换到 swap 后有泄密的可能。

其中锁定内存的动作由 mlock(3) 函数来完成。

----- 设置锁定内存大小,直接执行会报错
# ulimit -H -l 64
# ulimit -S -l 4

----- 然后执行测试程序
$ ./tulimit -l
lock memory failed, Cannot allocate memory

----- 将锁定内存大小软上限提高
# ulimit -S -l 8

----- 然后重新执行执行测试程序
$ ./tulimit -l

注意,实际上要多于 2~3KB 大小,因为有其它的动态库在使用 mlock 。

最大文件

通过 /proc/sys/fs/file-max 可以查看整个系统可以使用的文件句柄数量,可以通过 echo 1000000 > /proc/sys/fs/file-max 临时修改,或者在 /etc/sysctl.conf 中设置 fs.file-max = 1000000 永久修改。

其中 /proc/sys/fs/nr_open 会标识单个进程可分配的最大文件数,实际的设置值为 ulimit -n ,默认是软资源限制,可以通过 ulimit -Hn 查看硬资源限制。

----- 设置锁定内存大小,直接执行会报错
# ulimit -H -n 512
# ulimit -S -n 128

----- 然后执行测试程序
$ ./tulimit -n
... ...
info : #124 files opened.
error: open file failed, Too many open files.

信号挂起数

Pending Signals 可以针对所有的用户,用于设置可以被挂起、阻塞的最大信号量,注意,这里使用的是实时信号,非实时的只能接收一次。

如下的示例中,默认是可以接收三次的,如果设置其大小,那么其接收的信号数将减小,就是说实际发送了三次,但是实际上只接受到了两次。

----- 执行测试程序,正常可以发送接收三次
$ ./tulimit -i
info : parent sleep 1 seconds.
info : signal 34 sent from child.
info : signal 34 sent from child.
info : signal 34 sent from child.
info : parent wake up.
info : handle signal 34.
info : handle signal 34.
info : handle signal 34.
info : exit.

----- 修改为接收2
$ ulimit -S -i 2

----- 然后重新测试
$ ./tulimit -i
info : parent sleep 1 seconds.
info : signal 34 sent from child.
info : signal 34 sent from child.
info : signal 34 sent from child.
info : parent wake up.
info : handle signal 34.
info : handle signal 34.
info : exit.

消息队列最大值

POSIX Message Queues 也就是对消息队列进行限制。

----- 修改为1000字节
$ ulimit -q 1000

----- 执行测试,实际至少需要1280字节,不过报错有点奇怪
$ ./tlimit -q
error: open POSIX message queue failed, Too many open files.

通过 strace 命令查看时,会发现在通过 mq_open() 申请内存时报错,也就是 ENOMEM ,其中需要的字节为 128*10

CPU 使用时间

也就是程序占用 CPU 的时间进行限制,注意,这里是占用 CPU 的耗时,如果程序中有类似 sleep 的操作,那么实际上不会计入 CPU 耗时的。

----- 将CPU占用的时间设置为2秒
$ ulimit -t 2

----- 然后执行测试
$ time ./tlimit -t
Killed

real    0m2.008s
user    0m1.997s
sys     0m0.004s

可以通过 time 命令查看。

感觉这里是配置的连续 CPU 使用多久,如果中间有主动的 sleep 实际上是无效的。

实时优先级限制

Real Time Priority 注意,仍然只针对普通用户,可以通过如下方式进行测试。

----- 尝试用实时优先级20运行sleep程序
$ chrt -f 20 sleep 3
chrt: failed to set pid 0's policy: Operation not permitted

----- 切换到root并调整优先级再进行测试
$ sudo su -
# ulimit -r 20
# su foobar
$ chrt -f 20 sleep 3

----- 如果以50运行程序,那么仍然会报错,也就是说ulimit的限制起了作用.
$ chrt -r 50 sleep 3
chrt: failed to set pid 0's policy: Operation not permitted

fork进程数限制

Max User Processes 同样只对普通用户有效,可以使用如下方式测试,此时会生成 14 个进程。

测试貌似还有些问题。

最大进程数

理论最大数

简单来说就是通过全局的段描述符统计,不过太复杂了,后面补充。

实际数目

Linux 中通过 Process Identification Value, PID 来标示进程,其类型为 pid_t 实际上就是 int 类型。

当前系统可创建的最大进程数可通过 /proc/sys/kernel/pid_max 方式查看,可以通过 sysctl -w kernel.pid_max=65535 命令进行修改。

最大线程数

通过 /usr/include/bits/local_lim.h 中的 PTHREAD_THREADS_MAX 宏定义最大线程数,一般来说,对于 LinuxThreads 一般是 1024;对于目前最常用的 ntpl 是没有硬性限制的,仅受限于系统资源。

这个系统的资源主要就是线程的 stack 所占用的内存,用 ulimit -s 可以查看默认的线程栈大小,一般情况下,这个值是 8M=8192KB

可以写一段简单的代码验证最多可以创建多少个线程

include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void func()
{
}

int main(void)
{
    int i = 0;
    pthread_t thread;

    while ( 1 )
    {
        if (pthread_create(&thread, NULL, func, NULL) != 0)
        {
            return;
        }

        i++;
        printf("i = %d\n", i);
    }

    return EXIT_SUCCESS;
}

理论最大线程数

对于 32 位系统来说,最大可创建 381 个线程左右,因为 32 位 Linux 下的进程用户空间是 3G 的大小,也就是 3072M,用 3072M/8M=384,但是实际上代码段和数据段等还要占用一些空间,这个值应该向下取整到 383,再减去主线程,得到 382。

为了突破内存的限制,可以有两种方法:

  • 用 ulimit -s 1024 减小默认栈大小;
  • 调用pthread_create的时候用pthread_attr_getstacksize设置一个较小的栈大小。