文章目录
- 服务器程序规范
- 日志
- rsyslogd 守护进程
- syslog函数
- openlog函数
- setlogmask函数
- closelog函数
- 用户
- 进程间的关系
- 进程组
- 会话
- 系统资源限制
- 改变工作目录和根目录
- 服务器程序后台化
服务器程序规范
Linux
服务器程序一般以后台进程(守护进程[daemon
])形式运行。它没有控制终端,因此不会意外接受到用户输入,守护进程的父进程通常是init
进程(PID为1)。- 服务器的调试和维护都需要一个专业的日志系统,
Linux
常用一个守护进程rsyslogd(syslogd的升级版)
来处理系统日志。 Linux
服务器程序一般以非root
的身份运行。如:mysqld
、httpd
、syslogd
等后台进程分别拥有自己的运行账户mysql
、apache
和syslog
。Linux
服务器程序通常能处理很多命令行选项,如果一次运行的选项太多,那么可以用配置文件来管理。配置文件一般放在/etc
目录下。Linux
服务器程序通常在启动时会生成一个记录该后台进程PID
的文件并存入/var/run
目录中。例如:syslogd
的PID
文件是/var/run/syslogd.pid
。
日志
rsyslogd 守护进程
既能接受用户进程输出的日志,也能接受内核日志。
- 用户进程调用
syslog函数
将日志输出到一个UNIX
本地域socket
类型(AF_UNIX
)的文件/dev/log
中,rsyslogd
则监听该文件以获得用户进程的输出。 - 内核日志由
printk
等函数打印至内核的环状缓存(ring buffer)
中,环状缓存的内容直接映射到/proc/kmsg
中,rsyslogd
读取该文件以获得内核日志。
syslog函数
应用程序通过 syslog函数
与 rsyslogd
守护进程通信:
#include<syslog.h>
void syslog( int priority, const char* message, ...);
// 采用可变参数(message 和 第三个参数……)来结构化输出
// priority:设施值与日志级别的按位或,设施值的默认值是 LOG_USER。
限于 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 函数中的默认设施值
setlogmask函数
程序在开发阶段可能需要输出很多调试信息,而发布之后又需要将这些调试信息关闭。
解决这个问题的方法并非是在发布后删除调试代码(日后可能还需要用到),而有更简单的做法——设置日志掩码,使日志级别大于掩码的日志信息被系统忽略。
setlogmask函数
就用以设置日志掩码:
#include<syslog.h>
int setlogmask( int maskpri );
// maskpri指定日志掩码值
// 该函数始终会成功,返回调用进程旧的日志掩码值
closelog函数
用以关闭日志功能:
#include<syslog.h>
void closelog();
用户
服务器中不同的用户有不同的权限,因此 获取or设置 当前进程的 真实用户ID(UID)、有效用户ID(EUID)、真实组ID(GID)、有效组(EGID) 就尤为重要:
#include <sys/types.h>
#include <unistd.h>
uid_t getuid();
uid_t geteuid();
gid_t getgid();
gid_t getegid();
int setuid( uid_t uid );
int seteuid( uid_t uid );
int setgid( gid_t gid );
int setegid( gid_t gid );
一个进程有两个 用户ID: UID
和 EUID
。EUID
方便了资源访问:使得 运行程序的用户 拥有 该程序的有效用户 的权限。如:任何用户都可以通过 su程序
修改自己的账户信息,这就不得不访问需要 root
权限的 /etc/passwd
文件。那么以普通用户身份启动的 su程序
如何能访问/etc/passwd
文件呢?
用 ls
命令可以查看到,su程序
的所有者是 root
,且被设置了 set-user-id标志
,即任何普通用户运行 su程序
时,su程序
的有效用户为 root
。有效用户为 root
的进程被称为 特权进程(privileged processes)。
类似的,EGID
为运行目标程序的组用户提供有效组的权限。
进程间的关系
进程组
Linux
下每个进程都隶属于一个进程组,PGID
即为它的 进程组ID。进程组将一直存在,知道其中所有进程都退出或者加入到其他线程组。每个进程组都有一个首领进程,其 PGID
和 PID
相同。
#include< unistd.h>
pid_t getpgid( pid_t pid );
// 成功返回 ID,失败返回 -1 并设置 errno
int setpgid( pid_t pid, pid_t pgid );
// 将 PID 为 pid 的进程的 PGID 设置为 pgid,若 pid 和 pgid 相等,则 pid 指定的进程将被设定为进程组首领
// 若 pid=0,则表示设置 当前进程 的 PGID 为 pgid
// 若 pgid=0,则使用 pid 作为目标进程的 PGID
// 成功时返回 0,失败返回 -1 并设置 errno
一个进程只能设置自己或者子进程的 PGID
,子进程调用 exec
系列函数后,父进程不再能设置它的 PGID
。
会话
一些有关联的进程组能形成一个会话(session
),下面的函数用以创建一个会话:
#include<unistd.h>
pid_t setsid( void );
// 成功时返回新的进程组的 PGID,失败则返回 -1 并设置 errno
该函数不能由进程组的首领进程调用,否则产生一个错误。对于非组首领的进程,创建新会话的同时且有如下额外效果:
- 调用进程成为会话的首领,此时该进程是新会话的唯一成员。
- 新建一个进程组,其
PGID
就是调用进程的PID
,调用进程成为该组的首领。 - 调用进程将甩开终端(如果有的话)。
Linux
进程并未提供所谓 会话ID(SID)
的概念,但将会话首领所在的进程组的 PGID
视为 SID
,并提供了如下函数来读取 SID
:
#include<unistd.h>
pid_t getsid( pid_t pid );
系统资源限制
Linux 上运行的程序都会受到资源限制的影响,比如物理设备限制(CPU数量、内存数量等)、系统策略限制(CPU时间等),以及具体实现的限制(文件名的最大长度)。这些系统资源限制可以通过下面的函数来读取和设置:
#include <sys/resource.h>
int getrlimit( int resource, struct rlimit* rlim );
int setrlimit( int resource, const struct rlimit* rlim );
// 成功时返回 0,失败时返回 -1 并设置 errno。
struct rlimit{rlim_t rlim_cur; // 指定资源的软限制,是一个建议性的,最好不要超越的限制,若超越限制,则系统可能向进程发送信号以终止其运行。rlim_t rlim_max; // 指定资源的硬限制,一般是软限制的上限。普通程序可以减小硬限制,只有以 root 身份运行的程序才能增加硬限制。// rlim_t是一个整数类型,描述资源级别。
};
除上述外:
- 还可以使用
ulimit
命令修改当前shell
环境下的资源限制(软限制或/和硬限制),这种修改将对该shell
启动的所有后续程序有效。 - 还可以通过修改配置文件来改变系统软限制和硬限制,这种修改是永久的。
改变工作目录和根目录
#include<unistd.h>
/* 获取进程当前工作目录 */
char* getcwd( char* buf, size_t size );
// buf指向的内存用于存储进程当前工作目录的绝对路径名,大小由 size 参数指定
// 如果当前工作的绝对路径长度(加上末尾的“\0”)超过了 size,则返回 NULL 并设置 errno 为 ERANGE。
// 若 buf 为 NULL 并且 size 非 0,则 getcwd 可能在内部使用 malloc 动态分配内存,并将进程的当前工作目录存储在其中,
// 此时我们需要自己释放 getcwd 在内部创建的这块内存。
// 成功返回一个指向目标存储区(buf指向的缓存区或者getcwd在内部动态创建的缓存区)的指针,失败返回 NULL 并设置 errno。/* 改变进程的工作目录 */
int chdir( const char* path );
// path 指定要切换到的目标目录
// 成功返回 0,失败返回 -1 并设置 errno/* 改变进程根目录 */
int chroot(const char* path);
// 参数和返回值同上,调用本函数后,仍需使用 chdir("/") 来将工作目录切换至新的根目录。
// 改变进程的根目录后,程序可能无法访问处于旧路径的文件or目录,
// 不过可以利用进程已经打开的文件描述符来访问调用 chroot 之后不能直接访问的文件。
// 只有特权进程才能改变根目录
服务器程序后台化
让一个进程以守护进程的方式运行:
#include<unistd.h>
int daemon(int nochdir, int noclose);
// nochdir 用于指定是否改变工作目录,为 0 则工作目录被设置为“/”(根目录),否则继续使用当前目录
// noclose 为 0 时,标准输入、输出、错误输出都被重定向到 /dev/null 文件,否则依然使用原来的设备
// 成功返回 0,失败返回 -1 并设置 errno