• Linux 高性能服务器编程——Linux服务器程序规范


    问题聚焦:
        除了网络通信外,服务器程序通常还必须考虑许多其他细节问题,这些细节问题涉及面逛且零碎,而且基本上是模板式的,所以称之为服务器程序规范。
        工欲善其事,必先利其器,这篇主要来探讨服务器程序的一些主要规范。



    概览:
    • Linux服务器程序一般以后台程序的形式运行,后台进程又称为守护进程。
    • Linux服务器程序一般以某个专门的非root身份运行。
    • Linux服务器程序通常是可配置的,命令行或者配置文件的形式。
    • Linux服务器程序通常会在启动的时候生成一个PID文件,以记录该后台进程的PID。
    • Linux服务器程序通常需要考虑系统资源和限制。



    日志

    守护进程:rsyslogd
    功能:接收用户进程输出的日志,又能接收内核日志。

    用户日志函数:syslog
    功能:生成系统日志,输出到一个UNIX本地域socket类型(AF_UNIX)的文件/dev/log中。rsyslogd则监听该文件以获取用户进程的输出。

    内核日志函数:printk
    功能:打印至内核的环状缓存中,环状缓存的内容直接映射到/proc/kmsg文件中。rsyslogd则通过读取该文件获得内核日志。

    处理流程如下图所示:


    rsyslogd守护进程在接收到用户进程或内核输入的日志后,会把它们输出至某些特定的日志文件。默认情况下,调试信息则保存至/var/log/debug 文件,普通信息保存至/var/log/messages 文件,内核消息则保存至/var/log/kern.log 文件。不过,日志信息具体如何分发,可以在rsyslogd的配置文件中设置。rsyslogd的主配置文件是/etc/rsyslog.conf,其中主要可以设置的项包括:内核日志及其监听端口(默认是514,见/etc/services 文件),是否接收TCP日志及其监听端口,日志文件的权限,包含哪些子配置文件(比如/etc/rsyslog.d/*.conf)。rsyslogd的子配置文件则指定各类日志的目标存储文件。



    syslog函数

    功能:应用程序使用syslog函数与rsyslogd守护进程通信。
    函数定义:
    #include <syslog.h>
    void syslog ( int priority, const char* message, ... );    // 可变参数
    函数说明:
    第二个和第三个参数为可变参数,为了结构化输出
    priority参数:设施值与日志级别的按位或。设施值的默认值是LOG_USER,可选值如下:
    #include <syslog.h>
    #define LOG_EMERG            0      /* 系统不可用 */
    #define LOG_ALERT              1      /* 报警,需要立即采取动作 */
    #define LOG_CRIT                 2      /* 非常严重的情况 */
    #define LOG_ERR                  3     /* 错误 */
    #define LOG_WARNING       4     /* 警告 */
    #define LOG_NOTICE           5      /* 通知 */
    #define LOG_INFO               7      /* 信息 */
    #define LOG_DEBUG           8      /* 调试 */

    函数:openlog
    声明:
    #include <syslog.h>
    void openlog ( const char* ident, int logopt, int facility );
    作用:改变syslog的默认输出方式,进一步结构化日志内容
    函数说明:
    ident:指定的字符串将被添加到日志消息的日期和时间之后,通常被设置为程序的名字。
    logopt:对后续syslog调用的行为进行配置,可取下列值的按位或:
    #define    LOG_PID        0x01            /* 在日志消息中包含程序PID */
    #define    LOG_CONS    0x02            /* 如果消息不能记录到日志文件,则打印至终端 */
    #define    LOG_ODELAY    0x04        /* 延迟打开日志功能直到第一次调用syslog */
    #define    LOG_NDELAY    0x08        /* 不延迟打开日志功能 */
    facility:修改syslog函数中的默认设施值

    日志掩码:使日志级别大于日志掩码的日至信息被系统忽略。
    函数:
    #include <syslog.h>
    int setlogmask( int maskpri );
    函数说明:
    maskpri:指定日志掩码值。
    该函数始终会成功,返回调用进程先前的日志掩码值。

    例如:
    setlogmask(LOG_ERR); //仅仅记录ERR级别的日志消息
    setlogmask(LOG_UPTO(LOG_ERR)); //记录ERR以及之前的所有日志的消息[0,3]

    关闭日志功能:
    #include <syslog.h>
    void closelog();


    用户信息

    用户信息包括:
    • UID:实际用户ID
    • EUID:有效用户ID
    • GID:实际组ID
    • EGID:有效组ID
    #include <sys/types.h>
    #include <unistd.h>
    uid_t getuid();   //获取实际用户ID                 
    uid_t geteuid();  //获取有效用户ID
    gid_t getgid();  // 获取实际组ID
    gid_t getegid();  // 获取有效组ID
    int setuid( uid_t uid );  //设置实际用户ID
    int seteuid( uid_t uid );  //设置有效用户ID
    int setgid( gid_t gid );  // 设置实际组ID
    int setegid( gid_t gid );  // 设置有效组ID
    从函数名很容易看出函数的作用,就不解释了。

    说明一下UID和EUID的区别:
    一个进程拥有两个用户ID:UID和EUID。
    EUID存在的目的是方便资源访问,它使得运行程序的用户拥有该资源的有效用户的权限,比如root用户。

    实际用户指的是进程的执行者是谁,当用户使用用户名和密码成功登陆一个linux系统后就唯一确定其ID。
    有效用户ID指的是进程执行时对文件的访问权限。
    测试进程的UID和EUID的区别:
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    int main()
    {
        uid_t uid = getuid();
        uid_t euid = geteuid();
        printf("userid is %d,effective userid is %d
    ",uid,euid);
        return 0;
    }

    @ubuntu:~$ sudo chown root:root test_uid  // 修改目标文件的所有者为root
    @ubuntu:~$ sudo chmod +s test_uid    // 设置目标文件的set-user-id  标志     注意这段很重要
    @ubuntu:~$ ./test_uid  // 运行程序
    userid is 1000,effective userid is 0
    从测试程序的输出来看,进程的UID是启动程序的用户的ID,而EUID则是root账户(文件的所有者) 的ID。



    进程间关系

    进程组
    进程中:每个进程都隶属于一个进程组,所以每个进程还有一个进程组ID(PGID)。
    函数:
    #include <unistd.h>
    pid_t getpgid( pid_t pid );
    每个进程都有一个首领进程,其PGID和PID相同。进程组将一直存在,直到其中所有进程都退出,或者加入到其他进程中。

    设置PGID函数:
    #include <unistd.h>
    int setpgid( pid_t pid, pid_t pgid );
    作用:将PID为pid的进程的PGID设置为pgid。如果pid和pgid相同,则由pid指定的进程将被设置为进程组首领;如果pid为0,则表示设置当前进程的PGID为pgid;如果pgid为0,则使用pid作为目标PGID。setpgid函数成功时返回0,失败则返回-1并设置errno。

    一个进程只能设置自己或者其子进程的PGID,并且,当子进程调用exec系列函数后,我们也不能再在父进程中对它设置PGID。


    会话
    会话:一些有关联的进程组形成一个会话。
    函数:
    #include <unistd.h>
    pid_t setsid( void );
    函数说明:
    该函数不能由进程组的首领进程调用,否则将产生一个错误。对于非组首领的进程,调用该函数不仅创建新会话,而且有如下额外效果:
    • 调用进程成为会话的首领,此时该进程是该会话的唯一成员。
    • 新建一个进程组,其PGID就是调用进程的PID,调用进程成为该组的首领。
    • 调用进程将甩开终端(如果有的话)。
    会话ID即为首领所在进程组的PGID,获取会话ID(SID)函数:
    #include <unistd.h>
    pid_t getsid ( pid_t pid );

    用ps命令查看进程关系
    调用结果如图(PPID为父进程的PID):

    三个进程的关系如图:




    系统资源限制

    Linux系统资源限制可以通过如下一对函数来读取和设置:
    #include <sys/resource.h>
    int getrlimit( int resource, struct rlimit *rlim );
    int setrlimit( int resource, const struct rlimit *rlim );
    rlimit结构体的定义如下:
    struct rlimit
    {
        rlim_t rlim_cur;  //软限制
        rlim_t rlim_max;  //硬限制
    };
    函数说明:
    rlim_t 是一个整数类型,它描述资源级别。
    rlim_cur:指定资源的软限制。软限制是建议性的、最好不要超越的限制,如果超越的话,系统可能向进程发送信号以终止其运行。
    rlim_max:指定资源的硬限制。硬限制是软限制的上限。普通程序可以减小硬限制,而只有以root身份运行的程序才能增加硬限制。
    resource:指定资源限制类型。

    此外,我们可以使用ulimit命令修改当前shell环境下的资源限制(软限制或硬限制),这种修改将对该shell启动的所有后续程序有效。

    部分资源限制类型如下所示:




    改变工作目录和根目录

    获取当前工作目录和改变进程工作目录的函数分别是:
    #include <unistd.h>
    char* getcwd( char* buf, size_t size );
    int chdir( const char* path );
    函数说明:
    buf:指向的内存用于存储进程当前工作目录的绝对路径。
    size:指定buf的大小,如果当前工作目录的绝对路径的长度超过了size,则getcwd将返回NULL,并设置errno为ERANGE。
    path:指定要切换的目标目录。成功时返回0,失败返回-1并设置errno。

    改变进程根目录的函数是chroot,其定义如下:
    #include <unistd.h>
    int chroot( const char* path );
    函数说明:
    path:指定要切换的目标根目录。
    chroot并不改变进程的当前工作目录,所以调用chroot之后,我们仍需要使用chdir("/")来将工作目录切换至新的根目录。
    改变进程的根目录之后,程序可能无法访问类似/dev 的文件(和目录),因为这些文件(和目录)并非处于新的根目录之下。不过好在调用chroot之后,进程原先打开的文件描述符依然生效,所以我们可以利用这些早先打开的文件描述符来访问调用chroot之后不能直接访问的文件(和目录),尤其是一些日志文件。
    注意:只有超级用户才允许改变根目录,子进程将继承新的根目录。



    服务器程序后台化

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <fcntl.h>
    
    bool daemonize()
    {
    	pid_t pid = fork();
    	if (pid < 0) {
    		return false;
    	}
    	else if (pid > 0) {
    		exit(0);
    	}
    	
    	umask(0);
    
            //设置新的会话,设置本进程为进程组的首领
    	pid_t sid = setsid();
    	if (sid < 0)
    		return false;
    	
            //切换工作目录	
    	if ((chdir("/")) < 0)
    		return false;
    	
    	printf("2. pid: %ld, parent id: %ld
    ", (long)getpid(), (long)getppid());
    	
            //关闭标准输入,标准输出,标准错误输出
    	close(STDIN_FILENO);
    	close(STDOUT_FILENO);
    	close(STDERR_FILENO);
    
            //重定向标准输入,标准输出,标准错误输出到/dev/null
    	open("/dev/null", O_RDONLY);
    	open("/dev/null", O_RDWR);
    	open("/dev/null", O_RDWR);
    	return true;
    }
    
    int main(int argc, char **argv)
    {
    	printf("1. pid: %ld, parent id: %ld
    ", (long)getpid(), (long)getppid());;
    	daemonize();	
    	return 0;
    }

    daemon函数,实现了上述守护进程的功能:
    #include <unistd.h>
    int daemon(int nochdir, int noclose);
    nochdir参数用于指定是否改变工作目录,如果是0,则工作目录将设置为”/“, 否则继续使用当前工作目录。

    noclose参数为0时,标准输入,标准输出,标准错误输出都被重定向到/dev/null,否则依然使用原来的设备。

    调用成功时返回0,失败时返回-1,并设置errno。


    小结:
    这章虽然叫做程序规范,其实更多的是介绍一些开发过程,特别是中大型项目中很有用的技巧,可以提高我们的开发和调试效率。
    后面将会介绍一些常用的设计框架,这些都是比较基础的知识。后面如果还有时间,希望可以用我们学到的这些知识做一个小项目出来。也算是学以致用了。

    参考资料:
    《Linux高性能服务器编程》
  • 相关阅读:
    luogu4182 [USACO18JAN] Lifeguards P (单调队列优化dp)
    bzoj3277 串 (后缀数组+二分答案+ST表)
    [模板]后缀数组
    bzoj4361 isn (dp+树状数组+容斥)
    luogu4187 [USACO18JAN]Stamp Painting (dp)
    [USACO15DEC]高低卡(白金)High Card Low Card (Platinum)
    USACO环绕岛屿Surround the Islands 并查集 枚举暴力
    5.7 ~ 5.12 刷题列表
    5.4 ~ 5.6 刷题记录
    HNOI2012 永无乡 无旋Treap
  • 原文地址:https://www.cnblogs.com/hehehaha/p/6332340.html
Copyright © 2020-2023  润新知