Linux服务器程序规范
- Linux服务器程序一般以后台进程形式运行。后台进程又称守护进程。它没有控制终端,因而也不会意外接收到用户输入。 守护进程的父进程通常是init进程(PID为1的进程);
- Linux服务器程序通常有一套日志系统,它至少能输出日志到文件,有的高级服务器还能输出日志到专门的UDP服务器。大部分后台进程都在/var/log目录下拥有自己的日志目录;
- Linux服务器程序一般以某个专门的非root身份运行。比如mysqld、httpd、syslogd等后台进程,分别拥有自己的运行账户mysql、apache和syslog;
- Linux服务器程序通常是可配置的。服务器程序通常能处理很多命令行选项,如果一次运行的选项太多,则可以用配置文件来管理。绝大多数服务器程序都有配置文件,并存放在/etc目录下;
- Linux服务器进程通常会在启动的时候生成一个PID文件并存入/var/run目录中,以记录该后台进程的PID;
- Linux服务器程序通常需要考虑系统资源和限制,以预测自身能 承受多大负荷,比如进程可用文件描述符总数和内存总量等。
日志
服务器的调试和维护都需要一个专业的日志系统。Linux提供一个守护进程来处理系统日志——rsyslogd。rsyslogd守护进程既能接收用户进程输出的日志,又能接收内核日志。用户进程是通过调用syslog函数生成系统日志的。该函数将日志输出到一个UNIX本地域socket类型的文件/dev/log中, rsyslogd则监听该文件以获取用户进程的输出。rsyslogd守护进程在接收到用户进程或内核输入的日志后,会把它 们输出至某些特定的日志文件。默认情况下,调试信息会保存至var/log/debug文件,普通信息保存至/var/log/messages文件,内核消息则保存至/var/log/kern.log文件。
应用程序使用syslog函数与rsyslogd守护进程通信。syslog函数的定义如下:
#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 6/*信息*/
#define LOG_DEBUG 7/*调试*/
openlog可以改变syslog的默认输出方式:
#include<syslog.h>
void openlog(const char*ident,int logopt,int facility);
ident参数指定的字符串将被添加到日志消息的日期和时间之后,它通常被设置为程序的名字。logopt参数对后续syslog调用的行为进行配置。facility参数可用来修改syslog函数中的默认设施值。
日志的过滤也很重要。程序在开发阶段可能需要输出很多调试信息,而发布之后我们又需要将这些调试信息关闭。解决这个问题的方法并不是在程序发布之后删除调试代码(因为日后可能还需要用 到),而是简单地设置日志掩码,使日志级别大于日志掩码的日志信息被系统忽略。下面这个函数用于设置syslog的日志掩码:
#include<syslog.h>
int setlogmask(int maskpri);
maskpri参数指定日志掩码值。该函数始终会成功,它返回调用进程先前的日志掩码值。关闭日志功能:
#include<syslog.h>
void closelog();
用户信息
UID、EUID、GID和EGID
下面这一组函数可以获取和设置当前进程的真实用户ID(UID)、有效用户ID(EUID)、真实组ID(GID)和有效组ID(EGID):
#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*/
切换用户
下面的代码清单展示了如何将以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;
}
进程间关系
进程组
Linux下每个进程都隶属于一个进程组,因此它们除了PID信息外,还有进程组ID(PGID)。我们可以用如下函数来获取指定进程的PGID:
#include<unistd.h>
pid_t getpgid(pid_t pid);
该函数成功时返回进程pid所属进程组的PGID,失败则返回-1并设置errno。每个进程组都有一个首领进程,其PGID和PID相同。进程组将一直存在,直到其中所有进程都退出,或者加入到其他进程组。设置PGID:
#include<unistd.h>
int setpgid(pid_t pid,pid_t pgid);
setpgid 函数成功时返回0,失败则返回-1并设置errno。一个进程只能设置自己或者其子进程的PGID。并且,当子进程调用exec系列函数后,我们也不能再在父进程中对它设置PGID。
会话
一些有关联的进程组将形成一个会话(session)。下面的函数用于创建一个会话:
#include<unistd.h>
pid_t setsid(void);
该函数不能由进程组的首领进程调用,否则将产生一个错误。对于非组首领的进程,调用该函数不仅创建新会话,而且有如下额外效果:
- 调用进程成为会话的首领,此时该进程是新会话的唯一成员
- 新建一个进程组,其PGID就是调用进程的PID,调用进程成为该组的首领。
- 调用进程将甩开终端
该函数成功时返回新的进程组的PGID,失败则返回-1并设置errno。
用ps命令查看进程关系
执行ps命令可查看进程、进程组和会话之间的关系:
我们是在bash shell下执行ps和less命令的,所以ps和less命令的父进程是bash命令,这可以从PPID(父进程PID)一列看出。这3条命令创建 了1个会话(SID是2375)和2个进程组(PGID分别是2735和71265)。bash命令的PID、PGID和SID都相同,很明显它既是会话的首领,也是组2375的首领。ps命令则是组71256的首领,因为其PID也是71256;
系统资源限制
Linux上运行的程序都会受到资源限制的影响,比如物理设备限制 (CPU数量、内存数量等)、系统策略限制(CPU时间等),以及具体实现的限制(比如文件名的最大长度)。Linux系统资源限制可以通过如下一对函数来读取和设置:
#include<sys/resource.h>
int getrlimit(int resource,struct rlimit*rlim);
int setrlimit(int resource,const struct rlimit*rlim);
setrlimit和getrlimit成功时返回0,失败则返回-1并设置errno。rlim参数是rlimit结构体类型的指针,rlimit结构体的定义如下:
struct rlimit
{
rlim_t rlim_cur;
rlim_t rlim_max;
};
rlim_t是一个整数类型,它描述资源级别。rlim_cur成员指定资源的软限制,rlim_max成员指定资源的硬限制。软限制是一个建议性的、最好不要超越的限制,如果超越的话,系统可能向进程发送信号以终止其运行。例如,当进程CPU时间超过其软限制时,系统将向进程发送 SIGXCPU信号;当文件尺寸超过其软限制时,系统将向进程发送 SIGXFSZ信号。硬限制一般是软限制的上限。普通程序可以减小硬限制,而只有以root身份运行的程序才能增加硬限制。此外, 我们可以使用ulimit命令修改当前shell环境下的资源限制(软限制或/和硬限制),这种修改将对该shell启动的所有后续程序有效。我们也可以通过修改配置文件来改变系统软限制和硬限制,而且这种修改是永久。
resource参数指定资源限制类型。下列举了部分比较重要的资源限制类型:
改变工作目录和根目录
有些服务器程序还需要改变工作目录和根目录,例如Web服务器,一般来说,Web服务器的逻辑根目录并非文件系统的根目录“/”,而是站点的根目录。
获取进程当前工作目录和改变进程工作目录的函数分别是:
#include<unistd.h>
char*getcwd(char*buf,size_t size);
int chdir(const char*path);
buf参数指向的内存用于存储进程当前工作目录的绝对路径名,其大小由size参数指定。如果当前工作目录的绝对路径的长度超过了size,则getcwd将返回NULL,并设置errno为 ERANGE。如果buf为NULL并且size非0,则getcwd可能在内部使用 malloc动态分配内存,并将进程的当前工作目录存储在其中。如果是这种情况,则我们必须自己来释放getcwd在内部创建的这块内存。getcwd 函数成功时返回一个指向目标存储区的指针,失败则返回NULL并设置errno。
chdir函数的path参数指定要切换到的目标目录。它成功时返回0, 失败时返回-1并设置errno。
改变进程根目录的函数是chroot,其定义如下:
#include<unistd.h>
int chroot(const char*path);
path参数指定要切换到的目标根目录。它成功时返回0,失败时返 回-1并设置errno。chroot并不改变进程的当前工作目录,所以调用chroot之后,我们仍然需要使用chdir(“/”)来将工作目录切换至新的根目录。只有特权进程才能改变根目录。
服务器程序后台化
如何在代码中让一个进程以守护进程的方式运行? 守护进程的编写遵循一定的步骤,下面代码将服务器程序以守护进程的方式运行:
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 )
{
/* Log the failure */
return false;
}
close( STDIN_FILENO );
close( STDOUT_FILENO );
close( STDERR_FILENO );
open( "/dev/null", O_RDONLY );
open( "/dev/null", O_RDWR );
open( "/dev/null", O_RDWR );
return true;
}
实际上,Linux提供了完成同样功能的库函数:
#include<unistd.h>
int daemon(int nochdir,int noclose);
其中,nochdir参数用于指定是否改变工作目录,如果给它传递0,则工作目录将被设置为“/”(根目录),否则继续使用当前工作目录。noclose参数为0时,标准输入、标准输出和标准错误输出都被重定向到/dev/null文件,否则依然使用原来的设备。该函数成功时返回0,失败则返回-1并设置errno。