Linux 网络编程学习笔记——七、Linux 服务器程序规范

除了网络通信外,服务器程序通常还必须考虑许多其他细节问题。这些细节问题涉及面广且零碎,而且基本上是模板式的,所以称之为服务器程序规范。比如:

  • Linux 服务器程序一般以后台进程形式运行。后台进程又称守护进程(daemon)。它没有控制终端,因而也不会意外接收到用户输入。守护进程的父进程通常是 init 进程(PID 为 1 的进程)。
  • Linux 服务器程序通常有一套日志系统,它至少能输出日志到文件,有的高级服务器还能输出日志到专门的 UDP 服务器。大部分后台进程都在 /var/log 目录下拥有自己的日志目录。
  • Linux 服务器程序一般以某个专门的非 root 身份运行。比如 mysqld、httpd、syslogd 等后台进程,分别拥有自己的运行账户 mysql 、apache 和 syslog 。
  • Linux 服务器程序通常是可配置的。服务器程序通常能处理很多命令行选项,如果一次运行的选项太多,则可以用配置文件来管理。绝大多数服务器程序都有配置文件,并存放在 /etc 目录下。
  • Linux 服务器进程通常会在启动的时候生成一个 PID 文件并存入 /var/run 目录中,以记录该后台进程的 PID 。比如 syslogd 的 PID 文件是 /var/run/syslogd.pid 。
  • Linux 服务器程序通常需要考虑系统资源和限制,以预测自身能承受多大负荷,比如进程可用文件描述符总数和内存总量等。

一、日志

1
服务器的调试和维护都需要一个专业的日志系统。Linux 提供一个守护进程来处理系统日志:syslogd ,不 过现在的 Linux 系统上使用的都是它的升级版:rsyslogd 。

rsyslogd 守护进程既能接收用户进程输出的日志,又能接收内核日志。用户进程是通过调用 syslog 函数生成系统日志的。该函数将日志输出到一个 UNIX 本地域 socket 类型(AF_UNIX)的文件 /dev/log 中,rsyslogd 则监听该文件以获取用户进程的输出。内核日志在老的系统上是通过另外一个守护进程 rklogd 来管理的,rsyslogd 利用额外的模块现了相同的功能。内核日志由 printk 等函数打印至内核的环状缓存(ring buffer)中。环状缓存的内容直接映射到 /proc/kmsg 文件中。rsyslogd 则通过读取该文件获得内核日志。

rsyslogd 守护进程在接收到用户进程或内核输入的日志后,会把它们输出至某些特定的日志文件。默认情况下,调试信息会保存至 /var/log/debug 文件,普通信息保存至 /var/log/messages 文件,内核消息则保存至 /var/log/kern.log 文件。不过,日志信息具体如何分发,可以在 rsyslogd 的配置文件中设置。rsyslogd 的主配置文件是 /etc/rsyslog.conf ,其中主要可以设置的项包括:

  • 内核日志输入路径;
  • 是否接收 UDP 日志及其监听端口(默认是514,见 /etc/services 文件);
  • 是否接收 TCP 日志及其监听端口;
  • 日志文件的权限;
  • 包含哪些子配置文件(比如 /etc/rsyslog.d/*.conf),这些文件指定各类日志的目标存储文件。

应用程序使用 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/*调试*/

下面这个函数可以改变 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 函数中的默认设施值。

此外,日志的过滤也很重要。程序在开发阶段可能需要输出很多调试信息,而发布之后我们又需要将这些调试信息关闭。解决这个问题的方法并不是在程序发布之后删除调试代码(因为日后可能还需要用到),而是简单地设置日志掩码,使日志级别大于日志掩码的日志信息被系统忽略。下面这个函数用于设置 syslog 的日志掩码:

#include<syslog.h> 
int setlogmask(int maskpri);

maskpri 参数指定日志掩码值。该函数始终会成功,它返回调用进程先前的日志掩码值。最后,不要忘了使用如下函数关闭日志功能:

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

二、用户信息

用户信息对于服务器程序的安全性来说是很重要的,比如大部分服务器就必须以 root 身份启动,但不能以 root 身份运行。下面这一组函数可以获取和设置当前进程的真实用户 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*/

需要指出的是,一个进程拥有两个用户 ID:UID 和 EUID。EUID 存在的目的是方便资源访问:它使得运行程序的用户拥有该程序的有效用户的权限。比如 su 程序,任何用户都可以使用它来修改自己的账户信息,但修改账户时 su 程序不得不访问 /etc/passwd 文件,而访问该文件是需要 root 权限的。那么以普通用户身份启动的 su 程序如何能访问 /etc/passwd 文件呢?窍门就在 EUID 。用 ls 命令可以查看到,su 程序的所有者是 root ,并且它被设置了 set-user-id 标志。这个标志表示,任何普通用户运行 su 程序时,其有效用户就是该程序的所有者 root 。那 么,根据有效用户的含义,任何运行 su 程序的普通用户都能够访问 /etc/passwd 文件。有效用户为 root 的进程称为特权进程(privileged processes)。EGID的含义与EUID类似:给运行目标程序的组用户提供有效组的权限。

三、进程间的关系

1. 进程组

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 。

2. 会话

一些有关联的进程组将形成一个会话(session)。下面的函数用于创建一个会话:

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

该函数不能由进程组的首领进程调用,否则将产生一个错误。对于非组首领的进程,调用该函数不仅创建新会话,而且有如下额外效果:

  • 调用进程成为会话的首领,此时该进程是新会话的唯一成员。
  • 新建一个进程组,其 PGID 就是调用进程的 PID ,调用进程成为该组的首领。
  • 调用进程将甩开终端(如果有的话)。
  • 该函数成功时返回新的进程组的 PGID ,失败则返回 -1 并设置 errno 。

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

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

3. 用 ps 命令查看进程关系

执行 ps 命令可查看进程、进程组和会话之间的关系:

$ps-o pid,ppid,pgid,sid,comm|less 
PID PPID PGID SID COMMAND 
1943 1942 1943 1943 bash 
2298 1943 2298 1943 ps 
2299 1943 2298 1943 less

我们是在 bash shell 下执行 ps 和 less 命令的,所以 ps 和 less 命令的父进程是 bash 命令,这可以从 PPID(父进程 PID)一列看出。这 3 条命令创建了 1 个会话(SID 是 1943)和 2 个进程组(PGID 分别是 1943 和 2298)。bash 命令的 PID 、PGID 和 SID 都相同,很明显它既是会话的首领,也是组 1943 的首领。ps 命令则是组 2298 的首领,因为其 PID 也是 2298 :
2

四、系统资源限制

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

#include<sys/resource.h>

struct rlimit { 
	rlim_t rlim_cur; 
	rlim_t rlim_max; 
};

int getrlimit(int resource,struct rlimit*rlim); 
int setrlimit(int resource,const struct rlimit*rlim);
  • rlim_t:是一个整数类型,它描述资源级别;
  • rlim_cur:指定资源的软限制,软限制是一个建议性的、最好不要超越的限制,如果超越的话,系统可能向进程发送信号以终止其运行。例如,当进程 CPU 时间超过其软限制时,系统将向进程发送 SIGXCPU 信号;当文件尺寸超过其软限制时,系统将向进程发送 SIGXFSZ 信号;
  • rlim_max:指定资源的硬限制,硬限制一般是软限制的上限。普通程序可以减小硬限制,而只有以 root 身份运行的程序才能增加硬限制;
  • 此外可以使用 ulimit 命令修改当前 shell 环境下的资源限制(软、硬限制),这种修改将对该 shell 启动的所有后续程序有效。也可以通过修改配置文件来改变系统软、硬限制,这种修改是永久的;
  • resource:指定资源限制类型:
    资源限制类型含义
    RLIMIT_AS进程虚拟内存总量限制(单位是字节),超过该限制将使得某些函数产生 ENOMEN 错误
    RLIMIT_CORE进程核心转储文件(core dump)的大小限制(单位是字节),其值为 0 表示不产生核心转储文件
    RLIMIT_CPU进程 CPU 时间限制(单位是秒)
    RLIMIT_DATA进程数据段(初始化数据 data 段、未初始化数据 bss 段和堆)限制(单位是字节)
    RLIMIT_FSIZE文件大小限制(单位是字节),超过该限制将使得某些函数产生 EFBIG 错误
    RLIMIT_NOFILE文件描述符数量限制,超过该限制将使得某些函数产生 EMFILE 错误
    RLIMIT_NPROC用户能创建的进程数限制,超过该限制将使得某些函数产生 EAGAIN 错误
    RLIMIT_SIGPENDING用户能够挂起的信号数量限制
    RLIMIT_STACK进程栈内存限制(单位是字节),超过该限制将引起 SIGSEGV 信号
  • 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 ;
  • path:指定要切换到的目标目录;
  • chdir 成功时返回 0 , 失败时返回 -1 并设置 errno 。

改变进程根目录的函数是 chroot ,其定义如下:

#include<unistd.h>
int chroot(const char*path);
  • path:指定要切换到的目标根目录;
  • 成功时返回 0 ,失败时返回 -1 并设置 errno 。

chroot 并不改变进程的当前工作目录,所以调用 chroot 之后,仍然需要使用 chdir(“/”) 来将工作目录切换至新的根目录。改变进程的根目录之后,程序可能无法访问类似 /dev 的文件(和目录),因为这些文件(和目录)并非处于新的根目录之下。不过好在调用 chroot 之后,进程原先打开的文件描述符依然生效,所以可以利用这些早先打开的文件描述符来访问调用 chroot 之后不能直接访问的文件(和目录),尤其是一些日志文件。此外,只有特权进程才能改变根目录。

六、服务器程序后台化

最后,我们讨论如何在代码中让一个进程以守护进程的方式运行。守护进程的编写遵循一定的步骤,下面通过一个具体实现 来探讨,该程序将服务器程序以守护进程的方式运行:

bool daemonize() { 
	/*创建子进程,关闭父进程,这样可以使程序在后台运行*/ 
	pid_t pid=fork(); 
	if(pid<0) { 
		return false; 
	} else if(pid>0) { 
		exit(0); 
	} 
	/*设置文件权限掩码。当进程创建新文件(使用open(const char*pathname,int flags,mode_t mode)系统调用)时,文件的权限将是mode&0777*/ 
	umask(0); 
	/*创建新的会话,设置本进程为进程组的首领*/ 
	pid_t sid=setsid(); 
	if(sid<0) { 
		return false; 
	} 
	/*切换工作目录*/ 
	if((chdir("/"))0) { 
		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; 
}

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

#include<unistd.h>
int daemon(int nochdir,int noclose);
  • nochdir:用于指定是否改变工作目录,如果给它传递 0 ,则工作目录将被设置为根目录,否则继续使用当前工作目录。
  • noclose:为 0 时,标准输入、标准输出和标准错误输出都被重定向到 /dev/null 文件,否则依然使用原来的设备。
  • 成功时返回 0 ,失败则返回 -1 并设置 errno 。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BeZer0

打赏一杯奶茶支持一下作者吧~~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值