Linux服务器程序规范

日志

Linux系统日志总览

Linux提供一个守护进程来处理系统日志——$syslogd$,现在的$Linux$系统使用的都是它的升级版$rsyslogd$

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

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

内核日志在老的系统上是通过另外一个守护进程rklogd来管理的。

$rsyslogd$利用额外的模块现了相同的功能。内核日志由printk等函数打印至内核的环状缓存 (ring buffer)中。环状缓存的内容直接映射到$/proc/kmsg$文件中。 $rsyslogd$则通过读取该文件获得内核日志。

syslog函数

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

#include<syslog.h> 
void syslog(int priority,const char*message,...);

该函数采用可变参数(第二个参数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调用的行为进行配置,它可取下列值的按位或:

#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参数指定日志掩码值。该函数始终会成功,它返回调用进程先前的日志掩码值。

用来关闭日志:

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

用户信息

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);/*设置真实用户ID*/ 
int seteuid(uid_t uid);/*设置有效用户ID*/ 
int setgid(gid_t gid);/*设置真实组ID*/ 
int setegid(gid_t gid);/*设置有效组ID*/

一个进程拥有两个用户ID:UID和EUID。EUID :方便资源访问:它使得运行程序的用户拥有该程序的有效用户的权限

例如:su程序,任何用户都可以使用它来修改自己的账户信息,但修改账户时su程序不得不访问/etc/passwd文件,而访问该文件是需要root权限的。

用ls命令可以查看到,su程序的所有者是root,并且它被设置了set-user-id标志,这个标志表示,任何普通用户运行su程序时,其有效用户就是该程序的所有者root

有效用户为root的进程称为特权进程(privileged processes)

EGID:给运行目标程序的组用户提 供有效组的权限。

进程间关系

进程组

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);

该函数将PID为pid的进程的PGID设置为pgid。

  • 如果pid和pgid相 同,则由pid指定的进程将被设置为进程组首领;
  • 如果pid为0,则表示设置当前进程的PGID为pgid;
  • 如果pgid为0,则使用pid作为目标 PGID。
  • setpgid函数成功时返回0,失败则返回-1并设置errno。

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

会话(session)

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

创建一个会话:

#include<unistd.h> 
pid_t setsid(void);

不能由进程组的首领进程调用,否则将产生一个错误;

对于非组首领的进程,调用该函数不仅创建新会话,而且有如下额外效果:

  1. 调用进程成为会话的首领,此时该进程是新会话的唯一成员。
  2. 新建一个进程组,其PGID就是调用进程的PID,调用进程成为该组的首领。
  3. 调用进程将甩开终端(如果有的话)

函数成功时返回新的进程组的PGID,失败则返回-1并设置 errno。

Linux进程并未提供所谓会话ID(SID)的概念,但Linux系统认为它等于会话首领所在的进程组的PGID,并提供了如下函数来读取 SID:

#include<unistd.h> 
pid_t getsid(pid_t pid);

系统资源限制

Linux上运行的程序都会受到资源限制的影响,比如物理设备限制 (CPU数量、内存数量等)、系统策略限制(CPU时间等),以及具体实现的限制(比如文件名的最大长度)。

Linux系统资源限制可以通 过如下一对函数来读取和设置:

#include<sys/resource.h> 
int getrlimit(int resource,struct rlimit*rlim); 
int setrlimit(int resource,const struct rlimit*rlim);

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参数指定资源限制类型

setrlimit和getrlimit成功时返回0,失败则返回-1并设置errno。

改变工作目录和根目录

有些服务器程序还需要改变工作目录和根目录,一般来说,Web服务器的逻辑根目录并非文件系统 的根目录“/”,而是站点的根目录(对于Linux的Web服务来说,该目录 一般是/var/www/)

获取进程当前工作目录和改变进程工作目录的函数

#include<unistd.h> '
char*getcwd(char*buf,size_t size); 
int chdir(const char*path);

buf参数指向的内存用于存储进程当前工作目录的绝对路径名,其 大小由size参数指定。

  • 绝对路径的长度(再加上 一个空结束字符“\0”)超过了size,则getcwd将返回NULL,并设置 errno为ERANGE。
  • buf为NULL并且size非0,则getcwd可能在内部 使用malloc动态分配内存,并将进程的当前工作目录存储在其中,必须自己来释放getcwd在内部创建的这块内存
  • getcwd函数成功时返回一个指向目标存储区(buf指向的缓存区或 是getcwd在内部动态创建的缓存区)指针。
  • 失败则返回NULL并设置errno。

chdir函数的path参数指定要切换到的目标目录。它成功时返回0, 失败时返回-1并设置errno。

改变进程根目录的函数是chroot:

#include<unistd.h> 
int chroot(const char*path);

path参数指定要切换到的目标根目录。它成功时返回0,失败时返 回-1并设置errno。

chroot并不改变进程的当前工作目录,所以调用 chroot之后,仍然需要使用chdir(“/”)来将工作目录切换至新的根目录

改变进程的根目录之后,程序可能无法访问类似/dev的文件(和 目录),因为这些文件(和目录)并非处于新的根目录之下。不过好在调用chroot之后,进程原先打开的文件描述符依然生效,所以可以利用这些早先打开的文件描述符来访问调用chroot之后不能直接访问的文件(和目录),尤其是一些日志文件。

只有特权进程才能改变根目录。

服务器程序后台化

让一个进程以守护进程的方式运行:

将服务器程序以守护进程的方式运行。

fork函数有两个返回值,子进程中返回0,父进程中返回子进程pid。程序如果是在父进程中运行直接退出了,fork函数失败pid<0。

Linux提供了完成同样功能的库函数:

#include<unistd.h> 
int daemon(int nochdir,int noclose);
  • nochdir参数用于指定是否改变工作目录,如果给它传递 0,则工作目录将被设置为“/”(根目录),否则继续使用当前工作目录。
  • noclose参数为0时,标准输入、标准输出和标准错误输出都被重定向到/dev/null文件,否则依然使用原来的设备。 该函数成功时返回 0,失败则返回-1并设置errno。