0%

Linux服务器规范

Linux服务器规范

学习《Linux高性能服务器编程》第七章Linux服务器规范,为了印象深刻一些,多动手多实践,所以记下这个笔记。这一篇主要记录Linux中日志、用户信息、进程关系、改变工作目录和根目录。

日志

Linux系统日志

Linux上使用rsyslogd守护进程接收用户进程输出的日志和接收内核日志。

用户进程是通过syslogd函数生成系统日志。该函数将日志输出到一个UNIX本地域socket类型(AF_UNIX)的文件/dev/log中,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)。rsyslogd的子配置文件则指定各类日志的目标存储文件。

image-20220818105707879

rsyslogd系统日志功能比较复杂,有facility、priority、action等概念。还有Input模块、Filetr模块、Output模块等模块内容,目前还未弄清楚相关的知识。

查看了一下/etc/rsyslog.conf的配置文件

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# /etc/rsyslog.conf configuration file for rsyslog
#
# For more information install rsyslog-doc and see
# /usr/share/doc/rsyslog-doc/html/configuration/index.html
#
# Default logging rules can be found in /etc/rsyslog.d/50-default.conf


#################
#### MODULES ####
#################

module(load="imuxsock") # provides support for local system logging
# module(load="immark") # provides --MARK-- message capability

# provides UDP syslog reception
# module(load="imudp")
# input(type="imudp" port="514")

# provides TCP syslog reception
# module(load="imtcp")
# input(type="imtcp" port="514")

# provides kernel logging support and enable non-kernel klog messages
module(load="imklog" permitnonkernelfacility="on")


###########################
#### GLOBAL DIRECTIVES ####
###########################

#
# Use traditional timestamp format.
# To enable high precision timestamps, comment out the following line.
#
$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat

# Filter duplicated messages
$RepeatedMsgReduction on

#
# Set the default permissions for all log files.
#
$FileOwner syslog
$FileGroup adm
$FileCreateMode 0640
$DirCreateMode 0755
$Umask 0022
$PrivDropToUser syslog
$PrivDropToGroup syslog

#
# Where to place spool and state files
#
$WorkDirectory /var/spool/rsyslog

#
# Include all config files in /etc/rsyslog.d/
#
$IncludeConfig /etc/rsyslog.d/*.conf

我们可以看到模块module(load="imuxsock"),这是一个输入模块

然后看到默认规则在/etc/rsyslog.d/50-default.conf,查看一下

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
44
45
46
47
48
49
#  Default rules for rsyslog.
#
# For more information see rsyslog.conf(5) and /etc/rsyslog.conf

#
# First some standard log files. Log by facility.
#
auth,authpriv.* /var/log/auth.log
*.*;auth,authpriv.none -/var/log/syslog
#cron.* /var/log/cron.log
#daemon.* -/var/log/daemon.log
kern.* -/var/log/kern.log
#lpr.* -/var/log/lpr.log
mail.* -/var/log/mail.log
#user.* -/var/log/user.log

#
# Logging for the mail system. Split it up so that
# it is easy to write scripts to parse these files.
#
#mail.info -/var/log/mail.info
#mail.warn -/var/log/mail.warn
mail.err /var/log/mail.err

#
# Some "catch-all" log files.
#
#
#*.=debug;\
# auth,authpriv.none;\
# news.none;mail.none -/var/log/debug
#*.=info;*.=notice;*.=warn;\
# auth,authpriv.none;\
# cron,daemon.none;\
# mail,news.none -/var/log/messages

#
# Emergencies are sent to everybody logged in.
#
*.emerg :omusrmsg:*

#
# I like to have messages displayed on the console, but only on a virtual
# console I usually leave idle.
#
#daemon,mail.*;\
# news.=crit;news.=err;news.=notice;\
# *.=debug;*.=info;\
# *.=notice;*.=warn /dev/tty8

其中auth这种就是facility(设施),mail.err这种就是priority(等级),而这种设置日志记录的位置就是action

1
*.*;auth,authpriv.none          -/var/log/syslog

从上面这一个action可以看出普通的日志("."),设置日志记录位置是/var/log/syslog,前面-表示异步写入

我们使用logger命令测试一下

1
logger -i -t "my_test" "test_log"

使用vim查看/var/log/syslog可以看到我们的logger的结果

image-20220818172612033

rsyslogd的内容比较多,后续再补。

syslog函数

应用程序使用syslog函数和rsyslogd守护进程进行通讯。

1
2
3
4
5
#include <syslog.h>

void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void);

openlog用于改变syslog默认的输出方式,进行日志结构化

ident参数指定的字符串将被添加到日志消息的日期和时间之后,它通常被设置为程序的名字。

option参数对后续syslog调用的行为进行配置,它可取下列的值

1
2
3
4
5
6
#define	LOG_PID		0x01	/* log the pid with each message */
#define LOG_CONS 0x02 /* log on the console if errors in sending */
#define LOG_ODELAY 0x04 /* delay open until first syslog() (default) */
#define LOG_NDELAY 0x08 /* don't delay open */
#define LOG_NOWAIT 0x10 /* don't wait for console forks: DEPRECATED */
#define LOG_PERROR 0x20 /* log to stderr as well */

facility参数可以修改syslog函数中的默认设施值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* facility codes */
#define LOG_KERN (0<<3) /* kernel messages */
#define LOG_USER (1<<3) /* random user-level messages */
#define LOG_MAIL (2<<3) /* mail system */
#define LOG_DAEMON (3<<3) /* system daemons */
#define LOG_AUTH (4<<3) /* security/authorization messages */
#define LOG_SYSLOG (5<<3) /* messages generated internally by syslogd */
#define LOG_LPR (6<<3) /* line printer subsystem */
#define LOG_NEWS (7<<3) /* network news subsystem */
#define LOG_UUCP (8<<3) /* UUCP subsystem */
#define LOG_CRON (9<<3) /* clock daemon */
#define LOG_AUTHPRIV (10<<3) /* security/authorization messages (private) */
#define LOG_FTP (11<<3) /* ftp daemon */

/* other codes through 15 reserved for system use */
#define LOG_LOCAL0 (16<<3) /* reserved for local use */
#define LOG_LOCAL1 (17<<3) /* reserved for local use */
#define LOG_LOCAL2 (18<<3) /* reserved for local use */
#define LOG_LOCAL3 (19<<3) /* reserved for local use */
#define LOG_LOCAL4 (20<<3) /* reserved for local use */
#define LOG_LOCAL5 (21<<3) /* reserved for local use */
#define LOG_LOCAL6 (22<<3) /* reserved for local use */
#define LOG_LOCAL7 (23<<3) /* reserved for local use */

syslog用于输出日志。

priority参数是设施值和日志级别的按位与,默认是LOG_USER。日志级别有下面几个

1
2
3
4
5
6
7
8
#define	LOG_EMERG	0	/* system is unusable */
#define LOG_ALERT 1 /* action must be taken immediately */
#define LOG_CRIT 2 /* critical conditions */
#define LOG_ERR 3 /* error conditions */
#define LOG_WARNING 4 /* warning conditions */
#define LOG_NOTICE 5 /* normal but significant condition */
#define LOG_INFO 6 /* informational */
#define LOG_DEBUG 7 /* debug-level messages */

第二个参数message和第三个参数...来结构化输出。

closelog用于关闭日志

小例子:

1
2
3
4
5
6
7
8
#include <syslog.h>
int main(int argc, char **argv)
{
openlog(argv[0], LOG_CONS | LOG_PID, LOG_USER);
syslog(LOG_DEBUG, "This is a syslog test message generated by program '%s'\n", argv[0]);
closelog();
return 0;
}

结果

image-20220818183131617

用户信息

UID、EUID、GID和EGID

Linux中id真是太多了进程有pid,然后用户还有UID这种,真是有点绕。

在Linux当中一个进程(程序)拥有四个ID:真实用户UID、有效用户EUID、真实组GID和有效组EGID

这里以真实用户UID和有效用户EUID为例,真实组GID和有效组EGID道理是相同的。

EUID存在的目的是方便资源访问:它使得运行程序的用户拥有该程序的有效用户的权限(太过官方这种说法感觉)。EUID确定进程对某些资源和文件的访问权限。在大多数情况下,进程的UIDEUID是一样的,但是对于一些程序如supasswd这种set-user-id程序,它们有可能是不相同的。对于set-user-id程序而言,程序的EUID会变成程序的所有者UID,也就是说程序执行时,是以程序的所有者身份进行运行的。

passwd为例。passwd允许用户修改自己的登录密码,这个程序的所有者rootpasswd权限中有s,表明这是一个set-user-id程序。passwd命令需要修改/etc/shadow文件,对于/etc/shadow文件,普通用户是不可写(只有读权限)的,那么用户怎么能够通过passwd修改自己的密码呢,set-user-id程序的标志s就起到了作用,它在程序运行时将EUID会变成程序的所有者UID,那么程序有效的用户就会变成程序的所有者,在这里是root用户,理所当然的可以进行/etc/shadow文件的修改。

image-20220819104851240

再比如su程序允许任何用户都可以使用它来修改自己的账户信息,但修改账户时程序不得不访问文件/etc/passwd文件,而访问该文件是需要root权限的。那么以普通用户身份启动的su程序如何能访问/etc/passwd文件呢?

image-20220819130924743

su程序的所有者是root,并且它被设置了set-user-id标志。和上面passwd一样,set-user-id标志表示任何普通用户运行su程序时,其有效用户就是该程序的所有者root

获取和设置真实用户UID、有效用户EUID、真实组GID和有效组EGID的函数如下

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>
#include <sys/types.h>

uid_t getuid(void);
uid_t geteuid(void);
gid_t getgid(void);
gid_t getegid(void);
int setuid(uid_t uid);
int seteuid(uid_t euid);
int setgid(gid_t gid);
int setegid(gid_t egid);

为了测试上面所说,我们先创建一个普通用户bugcat,目前已经有普通用户ubuntu

可以看到bugcat的uid是1002

image-20220819140826977

我们写下读取程序uideuid的代码如下:

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

将其编译一下,然后查看查看文件属性,再运行程序,可以看到uideuid输出相同,表示真实用户有效用户都是ubuntu

image-20220819141010532

接着再将程序的所有者改为root,再加上s权限,再运行程序,可以看到uideuid输出不相同,表示真实用户ubuntu有效用户root(符合set-user-id程序特点)

image-20220819141209028

然后将程序的所有者改为bugcat(s权限不知道为啥自动取消了),再加上s权限,再运行程序,可以看到uideuid输出不相同,表示真实用户ubuntu有效用户bugcat

image-20220819141405860

最后我们去掉s权限,运行程序,可以看到uideuid输出相同,表示真实用户有效用户都是ubuntu,也从反面说明s权限的作用。

image-20220819142209993

进程间关系

进程组

Linux下每个进程都隶属于一个进程组,因此它们除了PID信息外,还有进程组ID(PGID)。我们可以用如下函数来获取指定进程PGID:

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

pid_t getpgid(pid_t pid);

该函数成功时返回进程pid所属进程组的PGID,失败则返回-1并设置errno

每个进程组都有一个首领进程,其PGIDPID相同。进程组将一直存在,直到其中所有进程都退出,或者加入到其他进程组。下面的函数用于设置PGID

1
int setpgid(pid_t pid, pid_t pgid);

该函数将PIDpid的进程的PGID设置为pgid

如果pidpgid相同,则由pid指定的进程将被设置为进程组首领;

如果pid为0,则表示设置当前进程的PGIDpgid

如果pgid为0,则使用pid作为目标PGID

setpgid函数成功时返回0,失败则返回-1并设置errno.

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

会话

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

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

pid_t setsid(void);

该进程不能由进程组的首领进程进行调用,会报错。

对于非进程组首领的进程,调用该函数不仅创建新会话,还会:

  • 调用进程成为会话的首领,此时该进程是新会话的唯一成员。
  • 新建一个进程组,其PGID就是调用进程的PID,调用进程成为该组的首领。
  • 调用进程将失去终端

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

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

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

pid_t getsid(pid_t pid);

使用ps命令查看进程之间的关系

在终端输入

1
ps -o pid,ppid,pgid,sid,comm | less

image-20220821164503929

它们之间的关系如下图

从单独的进程角度看,zsh是ps和less的父进程

从组的角度看,zsh是一个组(组里面只有zsh,所以zsh是进程组首领),ps和less是一个组(ps是进程组首领)

从会话的角度看,会话里面有两个关联的进程组,其实zsh是会话的首领

image-20220821165830395

改变工作目录和根目录

进程有工作目录和根目录。

工作目录:进程在哪个路径下被运行起来哪个路径就是进程的工作目录(Current Woring Directory, CWD)

根目录:就是”/“

工作目录和根目录可以通过/proc/PID/cwd/proc/PID/root进行查看

image-20220821175748810

工作目录和根目录都可以进行更改,获取进程当前工作目录和改变进程工作目录的函数分别是:

1
2
3
4
#include <unistd.h>

char *getcwd(char *buf, size_t size);
int chdir(const char *path);

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

如果当前工作目录的绝对路径的长度(再加上一个空结束字符“\0”)超过了size,则getcwd将返回NULL,并设置errnoERANGE

如果bufNULL并且size非0,则getcwd可能在内部使用malloc动态分配内存,并将进程的当前工作目录存储在其中。如果是这种情况,则我们必须自己来释放getcwd在内部创建的这块内存。

getcwd函数成功时返回一个指向目标存储区(buf指向的缓存区或是getcwd在内部动态创建的缓存区)的指针,失败则返回NULL并设置errno

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

改变进程根目录可以使用chroot:

1
2
3
#include <unistd.h>

int chroot(const char *path);

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

chroot并不改变进程的当前工作目录。

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