Linux高性能服务器-第七章-Linux服务器程序规范

87 阅读7分钟
  • 服务器程序一般以后台进程形式运行。
  • 服务器程序通常有一套日志系统,至少能输出日志到文件或者专门的UDP服务器。大部分后台进程在/var/log目录下拥有自己的日志目录。
  • 服务器程序通常以某个专门的非root身份运行。如mysqld,httpd等,拥有自己运行账户mysql, apache。
  • 服务器程序通常是可配置的。服务器程序通常能处理许多命令行选项,一次运行的选项过多时可用配置文件来管理。绝大多数服务器程序在/etc目录下有自己的配置文件。
  • 服务器程序通常在启动时生成一个PID文件并存入/var/run目录中,记录后台进程的PID。
  • 服务器程序通常需要考虑系统资源和限制,以预测自身能承受多大负荷。

7.1 日志

7.1.1 Linux日志系统

Linux使用守护进程rsyslogd来处理系统日志。

守护进程rsyslogd既能接收用户进程输出的日志,也能接收内核日志。

**接收用户进程日志:**用户进程通过syslog函数生成系统日志,该函数将日志输出到文件/dev/log(socket类型)中,rsyslogd则监听该文件以获取用户进程输出。

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

Linux系统日志体系:

rsyslogd.png

rsyslogd在收到系统日志后,会把他们分发到某些特定的日志文件中。默认情况下,调试信息会保存至/var/log/debug文件,普通信息保存至/var/log/message文件,内核消息保存至/var/log/kern.log文件。

可在rsyslogd的配置文件中设置日志信息的具体分发行为。其主配置文件是/etc/rsyslog.conf,主要可设置的项有:内核日志输入路径,是否接受UDP日志及其监听端口,是否接受TCP日志及其监听端口,日志文件的权限,包含哪些子配置文件(如/etc/rsyslog.d/*.conf)。子配置文件指定各类日志的目标存储文件。

7.1.2 syslog函数

应用程序使用syslog函数与rsyslogd进程通信。

#include <syslog.h>

/*
	priority: 设施值与日志级别按位或,设施值默认LOG_USER, 日志级别见下
*/
void syslog( int priority, const char* message, ... );

// 日志级别
#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	6
#define	LOG_DEBUG	7

openlog函数可改变syslog的默认输出方式,进一步结构化日志内容。

#include <syslog.h>
/*
	ident: 指定的字符串将被添加到日志消息的日期和时间之后,通常被设置为程序名
	logopt:对后续syslog调用的行为进行配置,取值见下
	facility:修改syslog中的默认设施值
*/
void openlog( const char* ident, int logopt, int facility );

// logopt取值
#define	LOG_PID			0x01		// 在日志消息中包含程序PID
#define	LOG_CONS		0x02		// 若消息不能记录到日志文件,则打印到终端
#define	LOG_ODELAY		0x04		// 延迟打开日志功能直到首次调用syslog
#define	LOG_NDELAY		0x08		// 不延迟打开日志功能

setlogmask函数用于设置syslog的日志掩码,进行日志过滤。

#include <syslog.h>
/* 
	日志级别大于日志掩码的日志信息将被系统忽略
	返回调用进程的旧日志掩码值。
*/
int setlogmask( int maskpri );

closelog函数用于关闭日志功能。

#include <syslog.h>
void closelog();

7.2 用户信息

7.2.1 UID、EUID和GID、EGID

用户信息对服务器程序的安全性很重要,如大部分服务器程序需以root身份启动,以非root身份运行。

#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 );
int seteuid( uid_t euid );
int setgid( gid_t gid );
int setegid( gid_t egid );

一个进程拥有两个用户ID: UID和EUID。

EUID的作用是方便资源访问,它使运行程序的用户拥有该程序有效用户的权限。如su程序,其有效用户是root,则普通用户运行su将拥有root权限。

EGID的含义与EUID类似,给运行程序的用户组提供程序有效组的权限。

7.2.2 切换用户

下面代码展示如何以root身份启动进程,再切换到以普通用户身份运行。

static bool switch_to_user( uid_t user_id, gid_t gp_id )
{
    // 先确保目标用户不是root
    if ( ( user_id == 0 ) && ( gp_id == 0 ) )
    {
        return false;
    }
	// 确保当前用户是合法用户:root或者目标用户
    gid_t gid = getgid();
    uid_t uid = getuid();
    if ( ( ( gid != 0 ) || ( uid != 0 ) ) && ( ( gid != gp_id ) || ( uid != user_id ) ) )
    {
        return false;
    }
	// 如果不是root 则已经是目标用户
    if ( uid != 0 )
    {
        return true;
    }
	// 切换到目标用户
    if ( ( setgid( gp_id ) < 0 ) || ( setuid( user_id ) < 0 ) )
    {
        return false;
    }

    return true;
}

7.3 进程间关系

7.3.1 进程组

Linux下每个进程都隶属于一个进程组,因此,进程除了PID外,还具有PGID。

#include <unistd.h>

// 获取进程的PGID
// 失败返回-1并设置errno
pid_t getpgid( pid_t pid );

每个进程组都有一个首领进程,其PGID与PID一致。

#include <unistd.h>

/*
	将pid进程的组ID设置为pgid
	若 pid = pgid, 则进程pid被设置为其进程组的首领进程
	pid = 0, 则设置当前进程的进程组ID为pgid
	pgid = 0, 则使用pid作为目标pgid
	成功返回0, 失败返回-1并设置errno
	进程只能设置自身或其子进程的PGID,且当子进程调用exec后,无法在父进程设置其PGID
*/
int setpgid( pid_t pid, pid_t pgid );
7.3.2 会话

一些有关联的进程组将形成一个会话(session)。

#include <unistd.h>

/*
	创建一个会话
	不能由进程组的首领进程调用,否则将产生一个错误
	非首领进程调用该函数不仅创建新会话,还将:
		调用进程成为会话首领,此时该进程是新会话的唯一成员
		新建一个进程组,其PGID为调用进程PID,调用进程成为该组首领
		调用进程将甩开终端
	成功返回新进程组PGID,失败返回-1并设置errno
*/ 
pid_t setsid( void );

// Linux中为提供SID的定义,但认为它等于会话首领所在的进程组PGID
pid_t getsid( pid_t pid );
7.3.3 ps查看进程关系

使用ps命令可查看进程、进程组和会话之间的关系

ps会话.png

7.4 系统资源限制

#include <sys/resource.h>
// 资源限制的读取和设置,resource指定资源限制类型,取值见下表
// 成功返回0, 失败返回-1并设置errno
int getrlimit( int resource, struct rlimit* rlim );
int setrlimit( int resource, const struct rlimit* rlim );

struct rlim 
{
    rlim_t rlim_cur;	// 软限制,建议性,超过系统可能发送信号终止
    rlim_t rlim_max;	// 硬限制,软限制的上限,普通程序可减小该值,只有root权限程序可增加
}

resource类型:

rlimit.png

7.5 改变工作目录和根目录

7.6 服务器程序后台化

bool daemonize()
{
    // 创建子进程,关闭父进程,这样可使程序在后台运行
    pid_t pid = fork();
    if ( pid < 0 )
    {
        return false;
    }
    else if ( pid > 0 )
    {
        exit( 0 );
    }
	// 设置文件权限掩码,当进程创建新文件(使用open),文件权限将是mode & 0777
    umask( 0 );
	// 创建新会话,设置本进程为进程组首领
    pid_t sid = setsid();
    if ( sid < 0 )
    {
        return false;
    }
	// 切换工作目录
    if ( ( chdir( "/" ) ) < 0 )
    {
        /* Log the failure */
        return false;
    }
	// 关闭标准输入设备、标准输出设备和标准错误输出设备
    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;
}

下面的库函数完成上述代码同样的功能

#include <unistd.h>

// nochdir: 指定是否改变工作目录,若为0,则将工作目录设置为根目录"/",否则不改变
// noclose: 为0时,标准输入、标准输出设备和标准错误输出重定向到/dev/null文件,否则不改变
// 成功返回0,失败返回-1并设置errno
int daemon( int nochdir, int noclose );