如何通过最简单的设置来实现最有效的性能调优,在有限资源的条件下保证程序的运作,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.conf
和 sysctl.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
也即是说:
- 用户进行登录时触发
pam_limits
模块; pam_limits
会读取limits.conf
中的配置,并设定用户所登陆 shell 的 limits;- 用户登陆 shell 之后,可以通过 ulimit 命令查看或者修改当前 shell 的 limits;
- 当用户在 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 采用的是虚拟内存,通常虚拟地址要远大于实际的物理内存,有时物理内存的数据会被换到交换区中,而有时交换区的内存会换到物理内存中。
常见的场景有:
- 出于性能考虑,例如数据库通常会将数据锁定到物理内存中。
- 安全需要,比如用户名、密码等内容被交换到 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设置一个较小的栈大小。