Linux服务器程序规范

Linux服务器程序规范

Linux系统日志

内核日志由一个守护进程rklogd管理,内核日志通过printk打印到内核缓存,映射到/proc/kmsg

syslogd(rsyslogd),另一个守护进程处理系统日志,获取/proc/kmsg文件读取内核日志

日志体系

用户信息

UID和EUID

UID(User Identifier):

  • UID是一个用于唯一标识用户的整数值。每个用户在系统中都有一个唯一的UID。
  • UID为0的用户是超级用户(root),具有系统中最高的权限。
  • 普通用户的UID通常从非零的正整数开始分配。

EUID(Effective User Identifier):

  • EUID是与进程关联的用户标识符,用于决定进程在执行时拥有哪个用户的权限。
  • 在许多情况下,EUID和UID是相同的,表示进程以指定用户的身份运行。但是,有一些情况下,EUID 可能会变得不同于 UID,以实现一些特殊的权限管理需求。
  • 当一个可执行程序具有设置用户ID位(Set-UID)时,该程序在执行时会以文件所有者的身份(EUID)运行,而不是运行程序的用户(UID)的身份。这允许普通用户在执行特定程序时获得超级用户的权限。

我们知道用户的密码都是存放在/etc/shadow文件下,我们看下这个文件的权限 ls -l /etc/shadow

1
2
❯ ls -l /etc/shadow
-rwxrwxrwx 1 root shadow 1449 Nov 19 00:46 /etc/shadow

可以看出这个文件的所有者是root

假如我是一个普通用户,显然我是可以修改我的密码的,通过passwd命令,无可厚非。自己修改自己的密码肯定是被允许的。但是仔细想想你会发现不对啊,我作为一个普通用户登录后,我的实际用户ID和有效用户ID都是我自己的UID。从上面可以看出,显然我不具有修改/etc/shadow文件的权限,那我执行passwd命令时怎么改我的密码的呢?

我们知道决定我们权限的是执行操作时的有效用户ID,所有我们在执行passwd命令时,我们的有效用户ID肯定被修改了。

1
2
❯ ls -l /usr/bin/passwd
-rwsr-xr-x. 1 root root 37288 Aug 3 2022 /usr/bin/passwd
    1. -rwsr-xr-x.: 这一部分描述文件的权限和类型。具体地说:
      • -: 这是一个普通文件,而不是目录(d)或链接(l)。
      • rws: 这表示用户(owner)有读、写和执行的权限。s 表示设置用户ID位(Set-UID)。
      • r-x: 这表示组用户(group)有读和执行的权限,但没有写的权限。
      • r-x: 这表示其他用户(others)有读和执行的权限,但没有写的权限。
      • .: 表示有额外的文件属性或扩展,通常是 SELinux 安全上下文或其他扩展属性。
    2. 1: 文件的链接数。在这种情况下,有一个链接指向该文件。
    3. root root: 这是文件的所有者(owner)和所属组(group)。在这里,文件的所有者是 root 用户,所属组也是 root。
    4. 37288: 文件的大小(以字节为单位)。
    5. Aug 3 2022: 文件的最后修改时间。
    6. /usr/bin/passwd: 文件的路径和名称。

关于 -rws 部分,当文件具有设置用户ID位(Set-UID)时,它将以文件所有者的身份运行而不是运行它的用户的身份。在这种情况下,passwd 可能以超级用户(root)的权限运行,以便允许用户更改自己的密码而无需超级用户的密码。 Set-UID 位用于提供程序执行过程中临时增加特权的能力。 这就是设置用户ID位的作用,它的存在就是为了普通用户在某些需要特权权限时,去临时的改变有效用户ID而获得特权权限。但是你可能有疑问,为什么我们不用setuid()直接修改呢?何苦绕这么大的弯子。但是如果可以使用setuid()来直接修改有效用户ID来获得特权权限,那么我们的特权权限就会不可控了。这违背了最小权限模型。

测试进程的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;
}

g++ testeuid.cpp -o test_uid

sudo chown root:root test_uid

sudo chmod +s test_uid

实验

切换用户

将以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
static bool switch_to_user( uid_t user_id, gid_t gp_id )
{
//先确保目标用户不是root
if ( ( user_id == 0 ) && ( gp_id == 0 ) )
{
return false;
}

gid_t gid = getgid();
uid_t uid = getuid();
//确保当前用户是合法用户:root或者目标用户
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;
}

进程间关系

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

  • ps: 进程查看命令。
  • -o pid,ppid,pgid,sid,comm: 使用-o选项指定要显示的进程信息列。具体含义如下:
    • pid: 进程ID(Process ID)。
    • ppid: 父进程ID(Parent Process ID)。
    • pgid: 进程组ID(Process Group ID)。
    • sid: 会话ID(Session ID)。
    • comm: 进程的命令名(命令行参数的第一个单词)。
  • | less: 使用管道将ps命令的输出传递给less命令,以便在屏幕上逐页查看输出。

因此,该命令的目的是显示每个进程的进程ID(pid)、父进程ID(ppid)、进程组ID(pgid)、会话ID(sid)和进程命令名(comm),并通过less命令进行分页显示。这样可以更方便地查看系统中运行的进程的相关信息。

1
2
3
4
5
6
7
8
9
10
11

PID PPID PGID SID COMMAND
235144 235138 235144 235144 zsh
235169 1 235168 235144 zsh
235196 1 235195 235144 zsh
235198 1 235195 235144 zsh
235199 235169 235168 235144 gitstatusd-linu
235452 235144 235451 235144 less
236714 235144 236714 235144 ps
236715 235144 236714 235144 less

从ppid可以看出,less和ps的父进程是zsh

进程间关系

pgid 是组 pid,等于组首领 pid sid 是 session id, 等于 session 首领 pid

session 主要是和终端相关,同一个终端启动的进程属于同一个 session

服务器程序后台化

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

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
bool daemonize()
{
//创建子进程关闭父进程,让程序在后台运行
pid_t pid = fork();
if ( pid < 0 )
{
return false;
}
else if ( pid > 0 )
{
exit( 0 );
}
//设置文件权限掩码
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;
}