0%

Linux服务器程序规范


Linux服务器程序规范包括:

  • Linux服务器程序一般以后台进程(守护进程)运行,没有控制终端,因而也不会意外接收到用户输入。守护进程的父进程通常是init进程(PID为1的进程)。
  • 日志系统,至少能输出日志到文件,有的高级服务器还能输出日志到专门的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服务器程序通常需要考虑系统资源和限制,以预测自身能承受多大负荷,比如进程可用文件描述符总数和内存总量等。

日志

Linux系统日志

守护进程syslogd,处理系统日志。现在的Linux系统上使用的都是它的升级版——rsyslogd。 rsyslogd守护进程既能接收用户进程输出的日志,又能接收内核日志。

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

内核日志由printk等函数打印至内核的环状缓存(ring buffer)中。环状缓存的内容直接映射到/proc/kmsg文件中。rsyslogd则通过读取该文件获得内核日志。

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

Linux的系统日志体系:

image-20220726200803717

syslog函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<syslog.h> 
void syslog(int priority, const char* message,...);
/*
参数:
priority:设施值与日志级别的按位或,设施值的默认值是LOG_USER
message, ...:可变参数,结构化输出
*/

// 日志级别:
#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的默认输出方式,进一步结构化日志内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<syslog.h>
void openlog(const char* ident, int logopt, int facility);
/*
参数:
ident:指定的字符串将被添加到日志消息的日期和时间之后,通常为程序的名字
logopt:对后续syslog调用的行为进行配 置,它可取下列值的按位或
facility:用来修改syslog函数中的默认设施值
*/

#define LOG_PID 0x01/*在日志消息中包含程序PID*/
#define LOG_CONS 0x02/*如果消息不能记录到日志文件,则打印至终端*/
#define LOG_ODELAY 0x04/*延迟打开日志功能直到第一次调用syslog*/
#define LOG_NDELAY 0x08/*不延迟打开日志功能*/

日志的过滤,设置日志掩码,使日志级别大于日志掩码的日志信息被系统忽略。

1
2
3
4
5
6
7
8
#include<syslog.h> 
int setlogmask(int maskpri);
/*
参数:
maskpri:指定日志掩码值
返回值:
该函数始终会成功,它返回调用进程先前的日志掩码值。
*/

关闭日志功能:

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

用户信息

UID、EUID、GID和EGID

UID:真实用户ID
EUID:有效用户ID
GID:真实组
EGID:有效组ID

1
2
3
4
5
6
7
8
9
10
#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程序时,其有效用户就是该程序的所有者root。任何运行su程序的普通用户都能够访问/etc/passwd文件。

有效用户为root的进程称为特权进程(privileged processes)。EGID的含义与EUID类似:给运行目标程序的组用户提供有效组的权限。

测试进程的UID和EUID的区别:

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>
#include <stdio.h>

int main()
{
uid_t uid = getuid();
uid_t euid = geteuid();
printf( "userid is %d, effective userid is: %d\n", uid, euid );
return 0;
}

编译该文件,将生成的可执行文件(名为test_uid)的所有者设置为root,并设置该文件的set-user-id标志,然后运行该程序以查看UID和EUID。具体操作如下:

1
2
3
4
$sudo chown root:root test_uid#修改目标文件的所有者为root
$sudo chmod+s test_uid#设置目标文件的set-user-id标志
$./test_uid#运行程序
userid is 1000,effective userid is:0

从测试程序的输出来看,进程的UID是启动程序的用户的ID,而 EUID则是root账户(文件所有者)的ID。

切换用户

将以root身份启动的进程切换为以一个普通用户身份运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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)。

1
2
3
4
5
6
7
8
9
#include<unistd.h>
pid_t getpgid(pid_t pid);
/*
作用:获取指定进程的PGID
参数:
pid:进程识别号
返回值:
返回进程pid所属进程组的PGID,失败返回-1
*/

每个进程组都有一个首领进程,其PGID和PID相同。进程组将一直存在,直到其中所有进程都退出,或者加入到其他进程组。

1
2
3
4
5
6
7
8
9
10
#include<unistd.h> 
int setpgid(pid_t pid, pid_t pgid);
/*
作用:设置PGID,将PID为pid的进程的PGID设置为pgid
参数:
pid:进程号
pgid:进程组号
返回值:
成功返回0,失败返回-1
*/

如果pid和pgid相同,则由pid指定的进程将被设置为进程组首领;如果pid为0,则表示设置当前进程的PGID为pgid;如果pgid为0,则使用pid作为目标PGID。

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

会话

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

1
2
3
4
5
6
7
#include<unistd.h> 
pid_t setsid(void);
/*
作用:创建一个会话
返回值:
成功时返回新的进程组的PGID,失败则返回-1
*/

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

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

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

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

用ps命令查看进程关系

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

1
2
3
4
5
$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。

进程间关系:

image-20220726214000802

系统资源限制

读取和设置Linux系统资源限制的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<sys/resource.h> 
int getrlimit(int resource, struct rlimit* rlim);
/*
作用:获取系统资源限制
参数:
resource:指定资源限制类型,见表7-1
rlim:指向rlimit结构体
返回值:
成功时返回0,失败则返回-1
*/

int setrlimit(int resource, const struct rlimit* rlim);
/*
作用:设置系统资源限制
参数,返回值:参考getrlimit
*/

// rlimit结构体的定义
struct rlimit {
rlim_t rlim_cur; // 描述资源级别,指定资源的软限制,软限制是一个建议性的、最好不要超越的限制,如果超越的话,系统可能向进程发送信号以终止其运行。
rlim_t rlim_max; // 指定资源的硬限制,软限制的上限,只有以root身份运行的程序才能增加硬限制
};

image-20220726214838702

改变工作目录和根目录

一般来说,Web服务器的逻辑根目录并非文件系统的根目录“/”,而是站点的根目录(对于Linux的Web服务来说,该目录一般是/var/www/)。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<unistd.h>
char* getcwd(char* buf, size_t size);
/*
作用:获取当前工作目录
参数:
buf:指向的内存存储当前工作路径的绝对路径名
size:buf路径的长度
返回值:
如果当前工作目录的绝对路径的长度(再加上一个空结束字符“\0”)超过了size,则getcwd将返回NULL,并设置errno为 ERANGE;
如果buf为NULL并且size非0,则getcwd可能在内部使用 malloc动态分配内存,并将进程的当前工作目录存储在其中。(需自己释放getcwd在内部创建的这块内存);
成功返回一个指向目标存储区的指针,失败返回NULL。
*/

int chdir(const char* path);
/*
作用:修改当前工作路径
参数:
path:指定要切换到的目标目录
返回值:
成功时返回0,失败时返回-1并设置errno
*/

int chroot(const char* path);
/*
作用:修改进程根目录
参数:
path:指定要切换到的目标根目录
返回值:
成功时返回0,失败时返回-1并设置errno
*/

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

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

服务器程序后台化

在代码中让一个服务器进程以守护进程的方式运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 )
{
/* 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;
}

实际上,Linux提供了完成同样功能的库函数:

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