1. 前言
本文不一定适合比较老版本的Linux,如果只关心使用,请直接看“总结”,本文主要针对CentOS,其它Linux发行版本类似,但细节可能有出入,比如重启服务可能不是用systemctl,而是service等。
当需要调整一个进程可打开的最多文件数或SOCKET连接数等,以CentOS为例,通常的做法是修改文件/etc/security/limits.conf,比如将最多可打开数调整为10万:
# vi /etc/security/limits.conf * soft nofile 100000 * hard nofile 100000 |
读取limit.conf文件的并不是Linux内核,而是一个内核模块PAM,对应的模块文件为:
/usr/lib64/security/pam_limits.so /usr/lib/security/pam_limits.so |
而/etc/pam.d目录下的配置文件,则由libpam.so读取,实际上所有的模块均由libpam.so加载,可将libpam.so看成是所有PAM模块的框架或容器,而且libpam.so本身也不是内核的组成部分。
多个不同Linux版本上查看,并没有叫libpam.so的文件名,均是libpam.so.0(不清楚是否所有都这样),但是编译Linux-PAM-1.3.1源代码有名为libpam.so软链接,指向libpam.so.0.84.2。
/usr/lib64/libpam.so.0 -> libpam.so.0.83.1 /usr/lib64/libpam.so.0.83.1 /usr/lib64/libpam_misc.so.0.82.0
/usr/lib/libpam.so.0 -> libpam.so.0.83.1 /usr/lib/libpam.so.0.83.1 /usr/lib/libpam_misc.so.0.82.0 |
libpam.so会被加载到crond等进程空间(那当然也可以不加载),如果没有加载libpam.so,则limits.conf不会生效。crond等不会主动加载libpam.so,那么是谁让libpam.so进入crond等进程空间的了?(执行“grep libpam /proc/`pidof crond`/maps”可查看libpam是否在crond的进程空间)。
在CentOS,可用service来启动或重启crond,所以跟它应当是相关的,而service实际调用的是systemctl这一系统工具(非Shell脚本,service为老版本使用方式,使用systemctl启动和重启服务,使用方式和service相同)。
# service crond restart Redirecting to /bin/systemctl restart crond.service
# file /bin/systemctl /bin/systemctl: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV)
# systemctl crond restart # 重启crontab服务进程crond |
2. PAM
PAM的全称为“Pluggable Authentication Modules”,即可插入认证模块。最初由太阳微系统公司(Sun Microsystems,已于2009年被甲骨文收购)于1995年在Solaris开发。PAM代码不包含在Linux内核中,并有专门的网站:http://linux-pam.org/,源代码托管在Github上(https://github.com/linux-pam/linux-pam/releases)。
3. pam_limits
pam_limits是PAM其中的一个模块(模块文件名为pam_limits.so),也是程序员接触较多的模型之一,对应的源代码文件为pam_limits.c,代码规模为几百行,加上所有注释和空格有1100多行:
#if !defined(linux) && !defined(__linux) #warning THIS CODE IS KNOWN TO WORK ONLY ON LINUX !!! #endif |
源代码提供autoconf编译,尝试在Linux-3.10上可编译成功:
~/Linux-PAM-1.3.1]$ ./configure --prefix=/usr/local/Linux-PAM-1.3.1 make |
4. limits.conf的由来
确定模块pam_limits的配置文件,由宏CONF_FILE决定:
// pam_limits.c #define CONF_FILE (pl->conf_file != NULL)?pl->conf_file:LIMITS_FILE |
使用的地方:
// pam_limits.c static int parse_config_file(pam_handle_t *pamh, const char *uname, uid_t uid, gid_t gid, int ctrl, struct pam_limit_s *pl) { FILE *fil; char buf[LINE_LENGTH];
/* check for the LIMITS_FILE */ if (ctrl & PAM_DEBUG_ARG) pam_syslog(pamh, LOG_DEBUG, "reading settings from '%s'", CONF_FILE); fil = fopen(CONF_FILE, "r"); // 打开配置文件,跟参数“pl”有关系 if (fil == NULL) { pam_syslog (pamh, LOG_WARNING, "cannot read settings from %s: %m", CONF_FILE); return PAM_SERVICE_ERR; } |
如果函数parse_config_file的参数“pl”值为NULL,则配置文件名在编译时决定,这种情况下,配置文件名被固定为limits.conf:
# Makefile.am modules/pam_limits/Makefile.am: -DLIMITS_FILE_DIR=\"$(limits_conf_dir)/*.conf\" \ modules/pam_limits/Makefile.am: -DLIMITS_FILE=\"$(SCONFIGDIR)/limits.conf\" |
只是limits.conf所在目录可由编译时决定,也就是看SCONFIGDIR,决定在automake的configure.ac文件:
# configure.ac AC_ARG_ENABLE(sconfigdir, AS_HELP_STRING([--enable-sconfigdir=DIR],[path to module conf files @<:@default=$sysconfdir/security@:>@]), SCONFIGDIR=$enableval, SCONFIGDIR=$sysconfdir/security) AC_SUBST(SCONFIGDIR)
dnl and some hacks to use /etc and /lib test "${prefix}" = "NONE" && prefix="/usr" if test ${prefix} = '/usr' then dnl If we use /usr as prefix, use /etc for config files if test ${sysconfdir} = '${prefix}/etc' then sysconfdir="/etc" fi |
推导出默认为“/etc/security/limits.conf”,但从前面的分析,可看到实际还可参数动态指定,这个参数怎么来?可进入Linux的/etc/pam.d目录,找一个看一看:
# vi /etc/pam.d/login session required pam_selinux.so close session required pam_selinux.so open |
上述最后一个配置项即为模型的参数值,参数值可有0、一个或多个。通常pam_limits.so使用默认参数值,因此它的配置文件limits.conf完整路径为:/etc/security/limits.conf。
5. 模块入口函数
会话(Session)类的PAM模块的入口函数均为pam_sm_open_session(授权类的为pam_sm_authenticate,密码类的为pam_sm_chauthtok),意为创建(打开)一个会话:
int pam_sm_open_session (pam_handle_t *pamh, int flags UNUSED, int argc, const char **argv); // libpam/pam_handlers.c: sym = "pam_sm_open_session"; |
加载模块在pam_handlers.c中完成,实际上一个模块可加载多次(可在/etc/security下看到有些配置文件中同一模型有多行)。类似于iptables,每加载一次创建一个handler,依次组成一个handler调用链(实际由配置文件中的每一行配置组成链):
// pam_handlers.c // 被_pam_parse_conf_file直接调用, // 和被_pam_init_handlers、_pam_load_conf_file一级间接调用 int _pam_add_handler(pam_handle_t *pamh , int handler_type, int other, int stack_level, int type , int *actions, const char *mod_path , int argc, char **argv, int argvlen) { struct loaded_module *mod = NULL; 。。。。。。 if ((handler_type == PAM_HT_MODULE || handler_type == PAM_HT_SILENT_MODULE) && mod_path != NULL) { if (mod_path[0] == '/') { mod = _pam_load_module(pamh, mod_path, handler_type); } else if (asprintf(&mod_full_path, "%s%s", DEFAULT_MODULE_PATH, mod_path) >= 0) { mod = _pam_load_module(pamh, mod_full_path, handler_type); _pam_drop(mod_full_path); } else { pam_syslog(pamh, LOG_CRIT, "cannot malloc full mod path"); return PAM_ABORT; } if (mod == NULL) { /* if we get here with NULL it means allocation error */ return PAM_ABORT; } 。。。。。。 /* point handler_p's at the root addresses of the function stacks */ switch (type) { 。。。。。。 case PAM_T_SESS: handler_p = &the_handlers->open_session; sym = "pam_sm_open_session"; handler_p2 = &the_handlers->close_session; sym2 = "pam_sm_close_session"; break; 。。。。。。 }
if ((mod_type == PAM_MT_DYNAMIC_MOD) && !(func = _pam_dlsym(mod->dl_handle, sym)) ) { pam_syslog(pamh, LOG_ERR, "unable to resolve symbol: %s", sym); } 。。。。。。 } |
每个模块的结果可能是成功PAM_SUCCESS(0),全定义在文件libpam/include/security/_pam_types.h中,下列展示小部分:
/* ----------------- The Linux-PAM return values ------------------ */ #define PAM_SUCCESS 0 /* Successful function return */ #define PAM_OPEN_ERR 1 /* dlopen() failure when dynamically */ /* loading a service module */ #define PAM_SYMBOL_ERR 2 /* Symbol not found */ #define PAM_SERVICE_ERR 3 /* Error in service module */ #define PAM_SYSTEM_ERR 4 /* System error */ |
6. 解析limits.conf
重聚焦到pam_limits模块,看看它的配置文件解析,这发生在函数pam_limits.c中的parse_config_file函数。
// pam_limits.c static int parse_config_file(pam_handle_t *pamh, const char *uname, uid_t uid, gid_t gid, int ctrl, struct pam_limit_s *pl) { FILE *fil; char buf[LINE_LENGTH]; // #define LINE_LENGTH 1024
// 以只读方式打开limits.conf fil = fopen(CONF_FILE, "r"); if (fil == NULL) { pam_syslog (pamh, LOG_WARNING, "cannot read settings from %s: %m", CONF_FILE); return PAM_SERVICE_ERR; }
/* start the show */ // 一行行遍历limits.conf while (fgets(buf, LINE_LENGTH, fil) != NULL) { line = buf; /* skip the leading white space */ while (*line && isspace(*line)) // 跳过空行 line++;
/* Rip off the comments */ tptr = strchr(line,'#'); // 去掉注释 if (tptr) *tptr = '\0'; /* Rip off the newline char */ tptr = strchr(line,'\n'); // 删除换行符,注意并不包括回车符 if (tptr) *tptr = '\0'; /* Anything left ? */ if (!strlen(line)) // 经过上面几步折腾,可能成了空行 continue;
// 直接调用sscanf解析配置项 // // 配置行示例: // * soft nofile 100000 // // domain:作用域名,“*”表示对所有用户有效 i = sscanf(line,"%s%s%s%s", domain, ltype, item, value); 。。。。。。 // 下面只看两个常用配置:domain配置为“*”或指定的用户名 // 可以看到在加载limits.conf,主要是设置输出参数pl的值。 // 而parse_config_file由pam_sm_open_session调用,亦即模块被加载时被调用。 // // 也因此修改limits.conf是不能立即生效的, // 除非重启该进程,而子进程又继承父进程的设置。 // // 假设程序跑在crontab中,则应重启crond进程, // 比如CentOS中重启crond:service crond restart // 虽然crontab中的进程是由crond拉起来的,但它并加载PAM模块, // 原因是crond在拉起子进程时,对子进程关闭了所有描述符。 // // process_limit针对当前调用进程进行limit设置 if (strcmp(domain, "*") == 0) // limit was set by a default entry process_limit(pamh, LIMITS_DEF_DEFAULT, ltype, item, value, ctrl, pl); 。。。。。。 if (strcmp(uname, domain) == 0) /* this user have a limit */ // limit was set by an user entry process_limit(pamh, LIMITS_DEF_USER, ltype, item, value, ctrl, pl); } } |
7. 生效limits.conf
加载PAM模块时,即会生效limits.conf,因为这个在pam_sm_open_session就已执行了:
/* now the session stuff */ int pam_sm_open_session (pam_handle_t *pamh, int flags UNUSED, int argc, const char **argv) { struct pam_limit_s plstruct; struct pam_limit_s *pl = &plstruct; 。。。。。。 // 调用parse_config_file解析limits.conf, // 配置行解析结果存储在pl中(亦即plstruct) retval = parse_config_file(pamh, pwd->pw_name, pwd->pw_uid, pwd->pw_gid, ctrl, pl); 。。。。。。 // 使配置立即生效(setup_limits调用系统函数setrlimit) retval = setup_limits(pamh, pwd->pw_name, pwd->pw_uid, ctrl, pl); 。。。。。。 return PAM_SUCCESS; } |
模块pam_limits.so是由PAM模块libpam.so加载的,crond加载的只是libpam.so。“/etc/pam.d”目录下的文件什么时候生效?加载libpam.so时生效:
// pam_start.c int pam_start ( const char *service_name, const char *user, const struct pam_conv *pam_conversation, pam_handle_t **pamh) { 。。。。。。 if ( _pam_init_handlers(*pamh) != PAM_SUCCESS ) { 。。。。。。 }
// pam_handlers.c int _pam_init_handlers(pam_handle_t *pamh) { 。。。。。。 // 函数_pam_parse_conf_file负责解析libpam.so的配置文件, // 这些配置文件一般位于目录/etc/pam.d下,如: // # ls -l /etc/pam.d/pass* // -rw-r--r-- 1 root root 188 6月 10 2014 /etc/pam.d/passwd // -rw-r--r-- 1 root root 974 12月 29 2016 /etc/pam.d/password-auth retval = _pam_parse_conf_file(pamh, f, NULL, PAM_T_ANY, 0); 。。。。。。 } |
8. systemctl和systemd
CentOS上的systemctl(CentOS-7.X之前为service脚本)类似于Windows平台的服务管理器,替代老版本中的service脚本来管理服务。Systemctl功能非常多,有关systemctl的功能不在本文过多描述。
sytemctl的工作原理是通过与服务systemd交互,来完成各项工作,比如重启crond进程。在CentOS,systemctl替代了inittab。
可以看到正是systemd加载了pam,从ldd结果可以看出systemd也不是动态加载pam模块,而是编译时就绑定了,因此libpam.so成了系统的必须部分(但pam_limits.so仍然不是,总是可插拔):
# ldd /usr/lib/systemd/systemd linux-vdso.so.1 => (0x00007ffce5b72000) /$LIB/libonion.so => /lib64/libonion.so (0x00007f2430f56000) libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f2430d31000) libcap.so.2 => /lib64/libcap.so.2 (0x00007f2430b2c000) libpam.so.0 => /lib64/libpam.so.0 (0x00007f243091d000) libaudit.so.1 => /lib64/libaudit.so.1 (0x00007f24306f5000) libkmod.so.2 => /lib64/libkmod.so.2 (0x00007f24304df000) libmount.so.1 => /lib64/libmount.so.1 (0x00007f24302a0000) librt.so.1 => /lib64/librt.so.1 (0x00007f2430098000) libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f242fe82000) libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f242fc66000) libc.so.6 => /lib64/libc.so.6 (0x00007f242f8a2000) /lib64/ld-linux-x86-64.so.2 (0x00007f243105c000) libdl.so.2 => /lib64/libdl.so.2 (0x00007f242f69e000) libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f242f43d000) liblzma.so.5 => /lib64/liblzma.so.5 (0x00007f242f218000) libattr.so.1 => /lib64/libattr.so.1 (0x00007f242f013000) libcap-ng.so.0 => /lib64/libcap-ng.so.0 (0x00007f242ee0d000) libz.so.1 => /lib64/libz.so.1 (0x00007f242ebf7000) libblkid.so.1 => /lib64/libblkid.so.1 (0x00007f242e9ba000) libuuid.so.1 => /lib64/libuuid.so.1 (0x00007f242e7b5000)
# ldd /usr/sbin/crond linux-vdso.so.1 => (0x00007ffef31a5000) /$LIB/libonion.so => /lib64/libonion.so (0x00007f87b89e5000) libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f87b8416000) libpam.so.0 => /lib64/libpam.so.0 (0x00007f87b8207000) libdl.so.2 => /lib64/libdl.so.2 (0x00007f87b8003000) libaudit.so.1 => /lib64/libaudit.so.1 (0x00007f87b7ddb000) libc.so.6 => /lib64/libc.so.6 (0x00007f87b7a17000) libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f87b77b6000) liblzma.so.5 => /lib64/liblzma.so.5 (0x00007f87b7591000) /lib64/ld-linux-x86-64.so.2 (0x00007f87b88cc000) libcap-ng.so.0 => /lib64/libcap-ng.so.0 (0x00007f87b738b000) libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f87b716f000) |
实际上,systemd为Linux系统(CentOS如此,像Ubuntu未必)的第一个进程,取代了以前的init进程,可以看到systemd进程和init进程不会同时存在,低版本为init,高版本为systemd。Ubuntu使用的是upstart,但也可能用systemd替代upstart。
在systemd源代码的编译文件meson.build(类似于CMake的CMakeLists.txt文件,或bazel的BUILD文件)中可以看到systemd对libpam的依赖。
systemctl部分用法:
1) 重启crond
# systemctl restart crond |
2) 显示系统状态
# systemctl status ● Jian.mooon State: degraded Jobs: 0 queued Failed: 2 units Since: 二 2017-10-24 02:38:50 CST; 1 years 3 months ago CGroup: / 。。。。。。 |
3) 重启系统
# systemctl reboot |
4) 关闭电源
# systemctl poweroff |
5) 待机
# systemctl suspend |
6) 休眠
# systemctl hibernate |
有关systemctl的更多信息,可浏览:
https://wiki.archlinux.org/index.php/systemd_(简体中文)。
9. 总结
修改limits.conf不会立即生效,除非重启相关的父进程,比如crontab的crond,而有些老版本的Linux可能只能重启以生效。
1) 系统启动 -> 启动初始化进程systemd -> 进程sytemd加载libpam.so模块 2) libpam.so根据/etc/pam.d决定是否加载pam_limits.so等 3) 在加载pam_limits.so时,会读取/etc/security/limits.conf 4) 重启crond等,实际是向systemd发重启指令 5) 一句话:如果要使用limits.conf生效,一定要有加载pam_limits.so,如果修改limits.conf,至少要让pam_limits.so重读limits.conf。 |
附1:资源
1) PAM官方
http://linux-pam.org/
2) PAM源代码
https://github.com/linux-pam/linux-pam/releases
3) systemd源代码
https://github.com/systemd/systemd(使用meson编译,Meson is an open source build system,依赖ninja)
4) Vixie-cron源代码
http://ftp.isc.org/isc/cron/
https://github.com/svagner/vixie-cron
ftp://ftp.riken.jp/Linux/cern/updates/slc52/SRPMS/repoview/vixie-cron.html
附2:编译ninja
ninja类似于make,使用meson之前必须先准备好ninja。
1) 从https://github.com/ninja-build/ninja下载ninja源代码
2) 解压源代码包,然后进入解压后的目录
3) 执行“./configure.py --bootstrap”
4) 成功后会在目录下生成名为ninja的可执行程序文件
5) 将可执行程序文件复制到PATH目录下,比如:/usr/local/bin或/usr/bin等目录
6) 完成。
附3:使用meson编译systemd
Meson-0.49.1要求3.5或更高版本的Python(https://www.python.org/),和1.5或更高版本的Ninja,还依赖gperf(简单安装:yum install -y gperf),还依赖libcap-dev(执行yum install -y libcap安装,如果仍然不行,从https://git.kernel.org/pub/scm/linux/kernel/git/morgan/libcap.git/下载源代码安装),除此之外还有一些其它的依赖,需逐个解决。
1) 从https://github.com/mesonbuild/meson下载meson源代码
2) 解压后,将meson目录添加到PATH中,比如:export PATH=/root/X/meson-0.49.1:$PATH
3) 进入systemd源代码目录
4) 执行“meson.py build”(如果出错,可能是Python版本不够)
5) 成功后会生成build子目录
6) 进入build目录,执行ninja开始编译(ninja类似于make)
附4:安装Python-3.7.2
Python-3.7.2采用automake编译:
1) 执行configure生成Makefile文件:./configure --prefix=/usr/local/Python-3.7.2
2) 执行make开始编译Python(编译时间会有点长)
3) 执行make install,安装Python(安装时间稍有点长)
4) 将Python的bin目录加入到PATH中,如:export PATH=/usr/local/Python-3.7.2/bin:$PATH
5) 可以开始使用Python-3.7.2了。
如果遇到错误“ModuleNotFoundError: No module named '_ctypes'”,是因为依赖的libffi-devel版本不够(可执行“yum install -y libffi-devel”安装libffi,或源码方式安装libffi)。
附5:安装libcap
1) 从https://git.kernel.org/pub/scm/linux/kernel/git/morgan/libcap.git/下载源代码包
2) 解压后进入解压目录
3) 执行make编译
4) 执行make install安装
5) 完成。