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,决定在automakeconfigure.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. systemctlsystemd

CentOS上的systemctlCentOS-7.X之前为service脚本)类似于Windows平台的服务管理器,替代老版本中的service脚本来管理服务。Systemctl功能非常多,有关systemctl的功能不在本文过多描述。

sytemctl的工作原理是通过与服务systemd交互,来完成各项工作,比如重启crond进程。在CentOSsystemctl替代了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)

 

实际上,systemdLinux系统(CentOS如此,像Ubuntu未必)的第一个进程,取代了以前的init进程,可以看到systemd进程和init进程不会同时存在,低版本为init,高版本为systemdUbuntu使用的是upstart,但也可能用systemd替代upstart

systemd源代码的编译文件meson.build(类似于CMakeCMakeLists.txt文件,或bazelBUILD文件)中可以看到systemdlibpam的依赖。

 

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不会立即生效,除非重启相关的父进程,比如crontabcrond,而有些老版本的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) 将Pythonbin目录加入到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) 完成。