- 服务器程序一般以后台进程形式运行。
- 服务器程序通常有一套日志系统,至少能输出日志到文件或者专门的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在收到系统日志后,会把他们分发到某些特定的日志文件中。默认情况下,调试信息会保存至/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命令可查看进程、进程组和会话之间的关系
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类型:
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 );