翼度科技»论坛 云主机 LINUX 查看内容

Unix 系统数据文件那些事儿

5

主题

5

帖子

15

积分

新手上路

Rank: 1

积分
15
前言

Unix like 系统和 windows 的最大区别就是有一套标准的系统信息数据文件,一般存放在 /etc/ 目录下,并且提供了一组近似的接口访问和查询信息,这些基础设施让系统管理看起来井井有条,下面就来盘点一下。
总览

下面这个表列出了 unix 系统常用的几种数据文件:
信息类别文件路径结构查询遍历
口令文件/etc/passwdpasswdgetpwnam / getpwuidsetpwent / getpwent / endpwent
阴影口令/etc/shadowspwdgetspnamsetspent / getspent / endspent
组文件/etc/groupgroupgetgrname / getgrgidsetgrent / getgrent / endgrent
主机/etc/hostshostentgethostbyname / gethostbyaddrsethostnet / gethostent / endhostent
网络/etc/networksnetentgetnetbyname / getnetbyaddrsetnetent / getnetent / endnetent
协议/etc/protocolsprotoentgetprotobyname / getprotobynumbersetprotoent / getprotoent / endprotoent
服务/etc/servicesserventgetservbyname / getservbyportsetservent / getservent / endservent
用户登录/var/run/utmp /var/log/wtmputmpgetutid / getutlinesetutent / getutent / endutent
从表中可以看到不论是查询还是遍历,接口具有某种一致性:

  • 查询接口遵循:getxxname / getxxbyname / getxxbyxx,name、xid 与 by 后面的关键字为 key,查询成功返回结构体指针,失败返回 NULL;
  • 遍历接口遵循:setxxent / getxxent / endxxent,其中:

    • set 用于 rewind 到文件开始,避免之前的调用移动遍历指针
    • get 第一次调用时打开文件,之后从上次遍历的位置向下遍历,直到结尾返回 NULL
    • end 用于明确关闭文件

有了上面的铺垫,下面分类来说明一下。
口令文件

在 CentOS 上 struct passwd 的定义位于  文件中:
  1. /* The passwd structure.  */
  2. struct passwd
  3. {
  4.   char *pw_name;                /* Username.  */
  5.   char *pw_passwd;              /* Password.  */
  6.   __uid_t pw_uid;               /* User ID.  */
  7.   __gid_t pw_gid;               /* Group ID.  */
  8.   char *pw_gecos;               /* Real name.  */
  9.   char *pw_dir;                 /* Home directory.  */
  10.   char *pw_shell;               /* Shell program.  */
  11. };
复制代码
其中 POSIX.1 标准只定义了其中 5 个:pw_name / pw_uid / pw_gid / pw_dir / pw_shell,大多数平台至少和 linux 一样包含了 7 个字段,有的甚至包含 10 个,例如 MacOS:
  1. struct passwd {
  2.         char        *pw_name;                /* user name */
  3.         char        *pw_passwd;                /* encrypted password */
  4.         uid_t        pw_uid;                        /* user uid */
  5.         gid_t        pw_gid;                        /* user gid */
  6.         __darwin_time_t pw_change;                /* password change time */
  7.         char        *pw_class;                /* user access class */
  8.         char        *pw_gecos;                /* Honeywell login info */
  9.         char        *pw_dir;                /* home directory */
  10.         char        *pw_shell;                /* default shell */
  11.         __darwin_time_t pw_expire;                /* account expiration */
  12. };
复制代码
多了 pw_class / pw_change / pw_expire。而 linux 中这些信息是存储在阴影口令文件中的,下一节再对它们进行说明。
注意 MacOS 中的 pwd.h 不位于 /usr/include 目录,可以使用以下命令定位系统头文件路径:
  1. > gcc -v -E -x c++ -                                                           
  2. Apple clang version 12.0.5 (clang-1205.0.22.11)
  3. Target: x86_64-apple-darwin20.6.0
  4. Thread model: posix
  5. InstalledDir: /Library/Developer/CommandLineTools/usr/bin
  6. "/Library/Developer/CommandLineTools/usr/bin/clang" -cc1 -triple x86_64-apple-macosx11.0.0 -Wdeprecated-objc-isa-usage -Werror=deprecated-objc-isa-usage -Werror=implicit-function-declaration -E -disable-free -disable-llvm-verifier -discard-value-names -main-file-name - -mrelocation-model pic -pic-level 2 -mframe-pointer=all -fno-strict-return -fno-rounding-math -munwind-tables -target-sdk-version=12.1 -fvisibility-inlines-hidden-static-local-var -target-cpu penryn -debugger-tuning=lldb -target-linker-version 650.9 -v -resource-dir /Library/Developer/CommandLineTools/usr/lib/clang/12.0.5 -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk -I/usr/local/include -stdlib=libc++ -internal-isystem /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/include/c++/v1 -internal-isystem /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/local/include -internal-isystem /Library/Developer/CommandLineTools/usr/lib/clang/12.0.5/include -internal-externc-isystem /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/include -internal-externc-isystem /Library/Developer/CommandLineTools/usr/include -Wno-reorder-init-list -Wno-implicit-int-float-conversion -Wno-c99-designator -Wno-final-dtor-non-final-class -Wno-extra-semi-stmt -Wno-misleading-indentation -Wno-quoted-include-in-framework-header -Wno-implicit-fallthrough -Wno-enum-enum-conversion -Wno-enum-float-conversion -Wno-elaborated-enum-base -fdeprecated-macro -fdebug-compilation-dir /Users/yunhai01/code/cnblogs -ferror-limit 19 -stack-protector 1 -fstack-check -mdarwin-stkchk-strong-link -fblocks -fencode-extended-block-signature -fregister-global-dtors-with-atexit -fgnuc-version=4.2.1 -fcxx-exceptions -fexceptions -fmax-type-align=16 -fcommon -fcolor-diagnostics -clang-vendor-feature=+disableNonDependentMemberExprInCurrentInstantiation -fno-odr-hash-protocols -mllvm -disable-aligned-alloc-awareness=1 -o - -x c++ -
  7. clang -cc1 version 12.0.5 (clang-1205.0.22.11) default target x86_64-apple-darwin20.6.0
  8. ignoring nonexistent directory "/Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/local/include"
  9. ignoring nonexistent directory "/Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/Library/Frameworks"
  10. #include "..." search starts here:
  11. #include <...> search starts here:
  12. /usr/local/include
  13. /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/include/c++/v1
  14. /Library/Developer/CommandLineTools/usr/lib/clang/12.0.5/include
  15. /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/include
  16. /Library/Developer/CommandLineTools/usr/include
  17. /Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/System/Library/Frameworks (framework directory)
  18. End of search list.
  19. ^C
复制代码
在 #include  search starts here 后的第一个包含 MacOS 版本号的 usr/include 的目录就是,这里是第三行:/Library/Developer/CommandLineTools/SDKs/MacOSX12.1.sdk/usr/include。
passwd 结构体的各个字段和数据文件中的字段是一一对应的,在 CentOS 上有以下的文件内容:
  1. > cat /etc/passwd
  2. root:x:0:0:root:/root:/bin/bash
  3. bin:x:1:1:bin:/bin:/sbin/nologin
  4. daemon:x:2:2:daemon:/sbin:/sbin/nologin
  5. adm:x:3:4:adm:/var/adm:/sbin/nologin
  6. lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
  7. sync:x:5:0:sync:/sbin:/bin/sync
  8. shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
  9. halt:x:7:0:halt:/sbin:/sbin/halt
  10. mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
  11. operator:x:11:0:operator:/root:/sbin/nologin
  12. games:x:12:100:games:/usr/games:/sbin/nologin
  13. ftp:x:14:50:FTP User:/:/sbin/nologin
  14. nobody:x:99:99:Nobody:/:/sbin/nologin
  15. systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
  16. dbus:x:81:81:System message bus:/:/sbin/nologin
  17. polkitd:x:999:998:User for polkitd:/:/sbin/nologin
  18. libstoragemgmt:x:998:997:daemon account for libstoragemgmt:/var/run/lsm:/sbin/nologin
  19. abrt:x:173:173::/etc/abrt:/sbin/nologin
  20. rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
  21. sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
  22. postfix:x:89:89::/var/spool/postfix:/sbin/nologin
  23. ntp:x:38:38::/etc/ntp:/sbin/nologin
  24. chrony:x:997:995::/var/lib/chrony:/sbin/nologin
  25. tcpdump:x:72:72::/:/sbin/nologin
  26. work:x:1000:1000::/home/work:/bin/bash
  27. centos:x:1001:1002:Cloud User:/home/centos:/bin/bash
复制代码
字段以冒号分隔,分别对应着 pw_name / pw_passwd / pw_uid / pw_gid / pw_gecos / pw_dir / pw_shell 字段,其中:

  • pw_name 是用户名。nobody 表示任何人都可以访问的账户,但只能访问 other 组设置权限的文件
  • pw_passwd 是加密后的口令,因安全问题已转移到阴影口令文件中,后面再说
  • pw_getcos 是 real name,放一些解释性的文字,可以为空
  • pw_dir 是初始目录,login 后所在的目录
  • pw_shell 是启动 shell,可以指定一些特殊的 shell 来禁止用户登录
nobody

在 CentOS 上,这个账户的用户 ID 和组 ID 都是 99,不提供任何特权;
在 Ubuntu 上这个值变为 65534:
  1. nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
复制代码
在 MacOS 上这个值变为 -2:
  1. nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
复制代码
nologin

pw_shell 如果指定以下程序,则表示禁止使用该账户登录系统:

  • /sbin/nologin
  • /usr/bin/false
  • /usr/bin/true
  • /dev/null
  • ……
上面的例子使用的是 nologin,mac 上使用 false 比较多一些。对比一下,使用 /sbin/nologin 可读性较优,登录时会打印一行提示信息:
  1. This account is currently not available.
复制代码
其次是 /dev/null:
  1. su: failed to execute /dev/null: Permission denied
复制代码
true / false 不返回任何信息,账户也不会切换。
空密码

pw_passwd 域在 CentOS 上永远保持 x,即使账户的密码为空也是如此,先来看看如何在 linux 创建空密码的账户:
  1. > sudo useradd mayun -d /home/mayun -m
  2. > sudo passwd -d mayun
  3. > sudo passwd -S mayun
  4. mayun NP 2022-10-30 1 99999 7 -1 (Empty password.)
  5. > su mayun
复制代码
并不像一些人想象的,useradd 不给 -p 参数就是空密码,此时新创建的账号无法登录,需要使用 passwd 设置密码后才可以。这里使用 passwd -d 选项删除账户密码,并通过 -S 选项验证 (Empty password.)。另外 useradd 中的 -d 和 -m 参数也是必需的 (-d 指定 pw_dir,-m 表示立即创建),不然在 Ubuntu 图形界面无法登录。查看 passwd 文件内容,增加了一行:
  1. mayun:x:1002:1003::/home/mayun:/bin/bash
复制代码
可见 pw_passwd 域仍为 'x',那空密码在哪里体现呢?请参考阴影口令一节。
ssh 免密登录

空密码的账号无法通过 ssh 登录:
  1. > ssh mayun@192.168.1.118
  2. mayun@192.168.1.118's password:
  3. Permission denied, please try again.
复制代码
因为这里 ssh 要求必需输入密码。可通过设置 ssh key 来实现免密登录,主要分以下几步。
1. 创建专门用于 ssh 免密登录的密钥对
  1. > ssh-keygen -b 4096 -t rsa
  2. Generating public/private rsa key pair.
  3. Enter file in which to save the key (/Users/yunhai01/.ssh/id_rsa): /Users/yunhai01/.ssh/id_rsa_ssh
  4. Enter passphrase (empty for no passphrase):
  5. Enter same passphrase again:
  6. Your identification has been saved in /Users/yunhai01/.ssh/id_rsa_ssh.
  7. Your public key has been saved in /Users/yunhai01/.ssh/id_rsa_ssh.pub.
  8. The key fingerprint is:
  9. SHA256:2M+iLH6QvLqETuJ+E88Jr5DrMKMUObZ/Y/f3ze1o9h0 yunhai01@bogon
  10. The key's randomart image is:
  11. +---[RSA 4096]----+
  12. |                 |
  13. |                 |
  14. |                 |
  15. |  .    o         |
  16. | = . .. S        |
  17. |..=o+    o       |
  18. |**. *o. . o    E |
  19. |O++ooX.o . .  =.+|
  20. |+*+**o= ... .+.==|
  21. +----[SHA256]-----+
复制代码
注意这里没有使用默认文件名 id_rsa,因为已经有访问 github 代码仓库的其它密钥存在,这里命名为 id_rsa_ssh 以做区分。
2. 将密钥同步到要登录的远程机器
  1. > ssh-copy-id -i .ssh/id_ras_ssh mayun@192.168.1.118
  2. /usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/Users/yunhai01/.ssh/id_rsa_ssh.pub"
  3. /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
  4. /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
  5. mayun@192.168.1.118's password:
  6. Number of key(s) added:        1
  7. Now try logging into the machine, with:   "ssh 'mayun@192.168.1.118'"
  8. and check to make sure that only the key(s) you wanted were added.
复制代码
注意这一步需要用户密码,所以必需暂时为 mayun 账户创建密码,稍后 ssh 连接成功后可以再删除。同步后的公钥将记录在远程账户 $HOME/.ssh/authorized_keys 文件中,用于稍后 sshd 的连接校验。
3. 指定密钥登录远程账户
  1. > ssh -i .ssh/id_rsa_ssh  mayun@192.168.1.118
  2. Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.15.0-53-generic x86_64)
  3. * Documentation:  https://help.ubuntu.com
  4. * Management:     https://landscape.canonical.com
  5. * Support:        https://ubuntu.com/advantage
  6. 642 updates can be applied immediately.
  7. To see these additional updates run: apt list --upgradable
  8. New release '22.04.1 LTS' available.
  9. Run 'do-release-upgrade' to upgrade to it.
  10. Your Hardware Enablement Stack (HWE) is supported until April 2025.
  11. Last login: Sat Nov 26 12:41:04 2022 from 192.168.1.18
  12. >
复制代码
注意这一步需要通过 -i 明确指定使用的密钥文件,否则还是需要输入密码。也可以通过 ssh config 配置文件来避免指定密钥:
  1. > cat ~/.ssh/config
  2. ……
  3. # ssh
  4. Host 192.168.1.118
  5. HostName 192.168.1.118
  6. User mayun
  7. IdentityFile ~/.ssh/id_rsa_ssh
复制代码
注意 Host 字段必需指定 ip,除非在 hosts 文件中进行了映射。
4. 总结
ssh 免密配置是用户到用户的,假设有两台机器 M 和 N,M 上分别有 U 和 P 两个账户,N 上分别有 S 和 T 两个账户,U 远程登录 S 需要设置一遍密钥,同机器的 P 想免密访问 S 也需要设置一遍,不能复用 U 的设置;同理,U 想要登录 T 也需要重新设置一遍,不能复用 S 的设置。U->S / U->T / P->S / P->T 这四对关系中,可以使用不同密钥,也可以使用相同密钥,即使使用相同密钥,S 和 T的 ~/.ssh/authorized_keys 文件中都会有两条记录,分别记录 U 和 T 的公钥。你学会了吗?
账号注释

pw_getcos 说是 real name,其实是一串可被解释的注释信息,例如使用 sudo vipw 编译 /etc/passwd 文件中的第 5 列:
  1. mayun:x:1002:1003:Jack Ma,Alibaba HangZhou China,12345678,18810245201:/home/mayun:/bin/bash
复制代码
为新增用户添加一些额外信息,再通过以下命令就可以展示这些信息:
  1. > finger -s mayun
  2. Login     Name       Tty      Idle  Login Time   Office     Office Phone   Host
  3. mayun     Jack Ma    pts/4       *  Oct 30 19:24 Alibaba Ha 12345678  
复制代码
可以看到显示了 Name / Office Address / Office Phone 三项,如果使用 -p 选项:
  1. > finger -p mayun
  2. Login: mayun                                  Name: Jack Ma
  3. Directory: /home/mayun                      Shell: /bin/bash
  4. Office: Alibaba HangZhou China, 12345678        Home Phone: +1-881-024-5201
  5. Last login Sun Oct 30 19:24 (CST) on pts/4
  6. No mail.
复制代码
可以展示额外的 Home Phone 信息,并且各个字段也能显示全了。不过 finger 已经是老古董命令了,即使在 CentOS 6.3 上也需要安装一下才能使用。
另外需要说明的是 vipw 命令,相比直接 vi /etc/passwd,它可以串行化对口令文件的更改,并且确保所做的更改与其它相关文件保持一致。
遍历顺序

使用 setpwent / getpwent / endpwent 接口遍历 passwd 数据文件时,得到的顺序是否和文件中记录的顺序一致?借用书上一个例子做个演示:
  1. #include <pwd.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <errno.h>
  6. #include "../apue.h"
  7. struct passwd* my_getpwnam (char const* name)
  8. {
  9.   struct passwd *ptr = 0;
  10.   setpwent ();
  11.   while ((ptr = getpwent ()) != NULL)
  12.   {
  13.     printf ("%s\n", ptr->pw_name);
  14.     if (strcmp (name, ptr->pw_name) == 0)
  15.       break;
  16.   }
  17.   endpwent ();
  18.   return (ptr);
  19. }
  20. int main(int argc, char *argv[])
  21. {
  22.      struct passwd pwd;
  23.      struct passwd *result;
  24.      if (argc != 2) {
  25.           fprintf(stderr, "Usage: %s username\n", argv[0]);
  26.           exit(EXIT_FAILURE);
  27.       }
  28.       result = my_getpwnam(argv[1]);
  29.       if (result == NULL) {
  30.           perror("getpwnam");
  31.           exit(EXIT_FAILURE);
  32.       }
  33.       pwd = *result;
  34.       printf("Name: [%p] %s; UID: %ld\n", pwd.pw_gecos, pwd.pw_gecos, (long) pwd.pw_uid);
  35.       exit(EXIT_SUCCESS);
  36. }
复制代码
这个例子演示了如何使用遍历接口模拟 getpwnam 的,这里主要的修改是在 my_getpwnam 中增加了对遍历用户名的输出,这样当给一个不存在的用户名后,就可以把整个文件过一遍啦:
  1. > ./getpwnam_ent abc > users.txt
复制代码
再对比 users.txt 与 /etc/passwd  的区别,在一台 Ubuntu 笔记本上,得到下面的结果:
查看代码
  1.  > paste -d':' users.txt /etc/passwd
  2. root:root:x:0:0:root:/root:/bin/bash
  3. daemon:daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
  4. bin:bin:x:2:2:bin:/bin:/usr/sbin/nologin
  5. sys:sys:x:3:3:sys:/dev:/usr/sbin/nologin
  6. sync:sync:x:4:65534:sync:/bin:/bin/sync
  7. games:games:x:5:60:games:/usr/games:/usr/sbin/nologin
  8. man:man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
  9. lp:lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
  10. mail:mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
  11. news:news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
  12. uucp:uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
  13. proxy:proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
  14. www-data:www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
  15. backup:backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
  16. list:list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
  17. irc:irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
  18. gnats:gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
  19. nobody:nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
  20. systemd-network:systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
  21. systemd-resolve:systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
  22. systemd-timesync:systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
  23. messagebus:messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
  24. syslog:syslog:x:104:110::/home/syslog:/usr/sbin/nologin
  25. _apt:_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
  26. tss:tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
  27. uuidd:uuidd:x:107:114::/run/uuidd:/usr/sbin/nologin
  28. tcpdump:tcpdump:x:108:115::/nonexistent:/usr/sbin/nologin
  29. avahi-autoipd:avahi-autoipd:x:109:116:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/usr/sbin/nologin
  30. usbmux:usbmux:x:110:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
  31. rtkit:rtkit:x:111:117:RealtimeKit,,,:/proc:/usr/sbin/nologin
  32. dnsmasq:dnsmasq:x:112:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
  33. cups-pk-helper:cups-pk-helper:x:113:120:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin
  34. speech-dispatcher:speech-dispatcher:x:114:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false
  35. avahi:avahi:x:115:121:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/usr/sbin/nologin
  36. kernoops:kernoops:x:116:65534:Kernel Oops Tracking Daemon,,,:/:/usr/sbin/nologin
  37. saned:saned:x:117:123::/var/lib/saned:/usr/sbin/nologin
  38. nm-openvpn:nm-openvpn:x:118:124:NetworkManager OpenVPN,,,:/var/lib/openvpn/chroot:/usr/sbin/nologin
  39. hplip:hplip:x:119:7:HPLIP system user,,,:/run/hplip:/bin/false
  40. whoopsie:whoopsie:x:120:125::/nonexistent:/bin/false
  41. colord:colord:x:121:126:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
  42. geoclue:geoclue:x:122:127::/var/lib/geoclue:/usr/sbin/nologin
  43. pulse:pulse:x:123:128:PulseAudio daemon,,,:/var/run/pulse:/usr/sbin/nologin
  44. gnome-initial-setup:gnome-initial-setup:x:124:65534::/run/gnome-initial-setup/:/bin/false
  45. gdm:gdm:x:125:130:Gnome Display Manager:/var/lib/gdm3:/bin/false
  46. yunh:yunh:x:1000:1000:yunh,Baidu Beijing China,010-82335469,13552560213:/home/yunh:/bin/bash
  47. systemd-coredump:systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
复制代码
结果是完全相同的,但在另外两台工作笔记本上,出现了不一致的结果,主要表现在两个方面:

  • 工作的 CentOS 虚拟机上遍历接口返回了更多的内容
  • 工作的 MacOS 笔记本上顺序与原文件不一致
下面是 CentOS 对比结果:
查看代码
  1.  > paste -d':' users.txt /etc/passwd
  2. root:root:x:0:0:root:/root:/bin/bash
  3. bin:bin:x:1:1:bin:/bin:/sbin/nologin
  4. daemon:daemon:x:2:2:daemon:/sbin:/sbin/nologin
  5. adm:adm:x:3:4:adm:/var/adm:/sbin/nologin
  6. lp:lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
  7. sync:sync:x:5:0:sync:/sbin:/bin/sync
  8. shutdown:shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
  9. halt:halt:x:7:0:halt:/sbin:/sbin/halt
  10. mail:mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
  11. operator:operator:x:11:0:operator:/root:/sbin/nologin
  12. games:games:x:12:100:games:/usr/games:/sbin/nologin
  13. ftp:ftp:x:14:50:FTP User:/:/sbin/nologin
  14. nobody:nobody:x:99:99:Nobody:/:/sbin/nologin
  15. systemd-network:systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
  16. dbus:dbus:x:81:81:System message bus:/:/sbin/nologin
  17. polkitd:polkitd:x:999:998:User for polkitd:/:/sbin/nologin
  18. libstoragemgmt:libstoragemgmt:x:998:997:daemon account for libstoragemgmt:/var/run/lsm:/sbin/nologin
  19. abrt:abrt:x:173:173::/etc/abrt:/sbin/nologin
  20. rpc:rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
  21. sshd:sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
  22. postfix:postfix:x:89:89::/var/spool/postfix:/sbin/nologin
  23. ntp:ntp:x:38:38::/etc/ntp:/sbin/nologin
  24. chrony:chrony:x:997:995::/var/lib/chrony:/sbin/nologin
  25. tcpdump:tcpdump:x:72:72::/:/sbin/nologin
  26. work:work:x:1000:1000::/home/work:/bin/bash
  27. centos:centos:x:1001:1002:Cloud User:/home/centos:/bin/bash
  28. mayun:mayun:x:1002:1003:Jack Ma,Alibaba HangZhou China,12345678,18810245201:/home/mayun:/bin/bash
  29. zhaomingfu:
  30. jiangze:
  31. shifanjie:
  32. yangmoda:
  33. zhuxiaoxi:
  34. xulei26:
  35. wangzishuo:
  36. yuehongda:
  37. yueguangbin:
  38. lifengjie:
  39. yugeyang:
  40. wangming04:
  41. houhuikun:
  42. liuxinran01:
  43. hanzecheng:
  44. yanghongjun:
  45. lizheyuan:
  46. zhanyongdong:
  47. huxiaoran01:
  48. liuchenghui01:
  49. yunhai01:
  50. liyanan14:
  51. suoning:
  52. panchenglong:
  53. shenhuiyang:
  54. donghan:
  55. chenyun05:
  56. xianghao01:
  57. zhouqi03:
  58. mengzhe:
  59. zhaokexin04:
  60. liuchao15:
  61. niukanglong:
  62. zhengyongpan:
  63. wangjunhan:
  64. shiyiyu:
  65. liuguangming:
  66. piaoxiaoyu:
  67. guochuanlei:
  68. hulingxuan:
  69. ranyunchao:
  70. liushuai06:
  71. songpeipei:
  72. guanzhicheng02:
  73. yuanxueran:
  74. liqilin01:
  75. lirui04:
  76. gaocongcong:
  77. jiahongpeng:
  78. wangyuanyuan14:
  79. chezhuo:
  80. huangfengzhi:
  81. yanxin08:
  82. tanrenzong:
  83. pankai01:
  84. wuyinping:
复制代码
可以看到通过接口得到的结果前半部分顺序是一致的,后半部分是多出来的。何时会出现接口返回比数据文件多的情况?摘录一段书中的原文作为解答:
用户和组数据是用网络信息服务 (Network Information Service, NIS) 实现的。这使管理员可以编辑数据库的主副本,然后将它自动分发到组织中的所有服务器上。客户端系统可以联系服务器查看用户和组的有关信息。NIS+ 和轻量级目录访问协议 (Lightweight Directory Access Protocol, LDAP) 提供了类似功能。很多系统通过配置文件 /etc/nsswitch.conf 来控制管理每一类信息的方法。

看上面例子中多出来的信息,确实和网络中真实的用户信息相吻合,这是第一种不一致的场景。
MacOS 上的情况更复杂一些,/etc/passwd 的内容比较多就不全贴出来了:
  1. > cat /etc/passwd | wc -l
  2.      120
复制代码
一共有 120 行,除去开头的注释是 110 条记录。再来看通过接口遍历的结果:
  1. > ./getpwnam_ent abc | wc -l
  2. getpwnam: Undefined error: 0
  3.      221
复制代码
居然有 221 行, 发现其中有大量重复记录,排序去重后变为 111 条记录:
  1. > ./getpwnam_ent abc | sort | uniq | wc -l
  2. getpwnam: Undefined error: 0
  3.      111
复制代码
将它和 /etc/passwd 去掉头部注释后的排序内容做个比较:
  1. > paste -d':' users.txt passwd.txt
  2. _amavisd:_amavisd:*:83:83:AMaViS Daemon:/var/virusmails:/usr/bin/false
  3. ......
  4. daemon:daemon:*:1:1:System Services:/var/root:/usr/bin/false
  5. nobody:nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
  6. root:root:*:0:0:System Administrator:/var/root:/bin/sh
  7. yunhai01:
复制代码
遍历结果只比数据文件多了一条记录:yunhai01,这正是我在这台 MacOS 上的账户名称。不过即使除去这条记录,原始的遍历顺序和数据文件也是不一致的,摘录书中一段话强行解释一下:
在 FreeBSD 中,……,还会产生该文件的散列版本。/etc/pwd.db 是 /etc/passwd 的散列版本,……。这些为大型系统提供了更好的性能。
散列版本应该是根据用户名或 uid 对内容进行排序以提高查找性能的副本,但是并没有在我的机器上找到 /etc/pwd.db 这个文件。出现重复记录确实是个问题,这样会导致对部分用户 (本例中除 yunhai01 外) 进行两次操作,属于系统级 bug。幸好对于 MacOS 来说,只在单用户模式下 (维护模式) 才会使用这些信息,平时都是通过 netinfo 存储的,问题不大。。
典型案例

补充一下接口使用案例,ls -l 选项因为需要根据 uid 展示用户名,用到了 getpwuid;login 程序因为需要根据用户名查询用户信息,用到了 getpwnam。
前者使用 strace 没有看到 getpwuid 调用:
  1. > strace ls -lh |& less
  2. ......
  3. open("/etc/passwd", O_RDONLY|O_CLOEXEC) = 3
  4. fstat(3, {st_mode=S_IFREG|0644, st_size=1276, ...}) = 0
  5. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1f48875000
  6. read(3, "root:x:0:0:root:/root:/bin/bash\n"..., 4096) = 1276
  7. read(3, "", 4096)                       = 0
  8. close(3)                                = 0
  9. ......
复制代码
只看到了 open /etc/passwd 的内容。不过这不能说明问题,毕竟 strace 只能跟踪系统调用,而 getpwuid 属于库函数,它底层也是通过打开 passwd 文件来查询信息的,因此不能说明什么。网上有一个通过 stat 模拟 ls -l 的例子,确实用到了 getpwuid 来显示用户信息,具体可参考附录。
login 是在用户登录时被调用的,strace 无从下口,只能改天拿来 linux 源码分析下了。。
阴影口令

先来探讨一下这个文件存在的必要性,我们都知道文件中存储的都是经过加密的口令,使用的是非可逆的加密算法,从密文无法倒推回明文,那为何还怕密文泄露呢?引用书上的一段话做个说明:
但是可以对口令进行猜测,将猜测的口令经过单向算法变换成加密形式,然后将其与用户的加密口令相比较……用户往往以非随机方式选择口令……一个经常重复的试验是先得到一份口令文件,然后用试探方法猜测口令
对这段话深有同感,有太多服务器或测试机使用了 123qwe!@#、1qaz@WSX、111qqq!!!… 这类符合操作系统要求却又简单好记的密码。如果将加密口令字段移入另外一个需要更高权限的单独文件中 (如 /etc/shadow),普通用户就无法获取用于猜测口令的原始信息从而避免了很多风险。访问阴影口令文件的程序会非常有限 (如 login / passwd),况且这些程序通常是设置用户 ID 为 root 的,也能正常运行 (关于 set-user-id,可以参考之前写的:《[apue] linux 文件访问权限那些事儿》)。
在 CentOS 上 struct spwd 的定义位于  文件中:
  1. struct spwd
  2. {
  3.     char *sp_namp;                /* Login name.  */
  4.     char *sp_pwdp;                /* Encrypted password.  */
  5.     long int sp_lstchg;                /* Date of last change.  */
  6.     long int sp_min;                /* Minimum number of days between changes.  */
  7.     long int sp_max;                /* Maximum number of days between changes.  */
  8.     long int sp_warn;                /* Number of days to warn user to change the password.  */
  9.     long int sp_inact;                /* Number of days the account may be inactive.  */
  10.     long int sp_expire;                /* Number of days since 1970-01-01 until account expires.  */
  11.     unsigned long int sp_flag;        /* Reserved.  */
  12. };
复制代码
阴影口令不是 POSIX.1 标准的一部分,大多数实现至少要求包含其中 2 个:sp_namp / sp_pwdp,其它字段用于控制口令改动频率 (sp_lstchg / sp_min / sp_max / sp_warn) 及账户保持活动状态的时间 (sp_inact / sp_expire),freebsd 和 MacOS 甚至没有阴影口令,账户的额外信息是放在 passwd 文件中的 (pw_change / pw_expire),而 linux 和 Solaris 在这一点上非常接近但是也有细微差别:

  • Solaris 中整数字段均定义均为 int;linux 上为 long int
  • sp_inact 在 Solaris 上表示用户上次登录以来所经过的天数;linux 上为口令过期的尚余天数
spwd 结构体的各个字段和数据文件中的字段是一一对应的,在 CentOS 上有以下的文件内容:
  1. > sudo cat /etc/shadow
  2. root:$6$hT9cNMJc$Ej4tEC3hSHv4jepws0wDgXbIO6lK6GOJ4Yzm1iECfKiq9Bl.zeoNCzr.bI7I3NhPnBezZTK51clj5LuzyXDXc1:18717:0:99999:7:::
  3. bin:*:17632:0:99999:7:::
  4. daemon:*:17632:0:99999:7:::
  5. adm:*:17632:0:99999:7:::
  6. lp:*:17632:0:99999:7:::
  7. sync:*:17632:0:99999:7:::
  8. shutdown:*:17632:0:99999:7:::
  9. halt:*:17632:0:99999:7:::
  10. mail:*:17632:0:99999:7:::
  11. operator:*:17632:0:99999:7:::
  12. games:*:17632:0:99999:7:::
  13. ftp:*:17632:0:99999:7:::
  14. nobody:*:17632:0:99999:7:::
  15. systemd-network:!!:17850::::::
  16. dbus:!!:17850::::::
  17. polkitd:!!:17850::::::
  18. libstoragemgmt:!!:17850::::::
  19. abrt:!!:17850::::::
  20. rpc:!!:17850:0:99999:7:::
  21. sshd:!!:17850::::::
  22. postfix:!!:17850::::::
  23. ntp:!!:17850::::::
  24. chrony:!!:17850::::::
  25. tcpdump:!!:17850::::::
  26. work:$6$NHiZrcs5$igsfZKouoJNEYJMezMfG.sDQYA4Xt6Nu1jEkfz/7/C1qs96aXiAsgRJoeYBo7fAf4oeUkV8T3424ZQ4RIrOix0:18058:1:99999:7:::
  27. centos:!!:18108:1:99999:7:::
  28. mayun::19295:1:99999:7:::
复制代码
字段仍以冒号分隔,做个简单说明:

  • sp_pwdp 除了密文口令外,还可以有以下选择:*、!!、空,其中除了空表示没有口令外,其它含义目前不清楚
  • sp_lstchg 是上次更新口令时间,单位是 1970.1.1 开始计算的天数,例如上例中 work 用户的值 18058 表示:1970+18058/365=2019.61,大概是 2019 年中,以上仅是粗略算法,精细一点的可以使用日期计算器
  • sp_min 是最小口令更改间隔,小于这个天数会被系统拒绝,0 表示随时改
  • sp_max 是最大口令更改间隔,超出这个天数系统会让用户强制更新密码,99999 大概是 274 年,终其一生应该不用改了
  • sp_warn 是过期前提醒天数,一般是一周内 (7),设置为 -1 表示不提醒
  • sp_inact 是过期后多少天内账号变为 inactive 状态,此时可登陆但不能操作,必需更新密码
  • sp_expire 是多少天后账号会过期,此时无法登陆
使用 chage 命令可以修改与账户改动频率控制相关的字段,感兴趣的可自行 man 查阅用法。
遍历结果

使用 setspent / getspent / endspent 对 shadow 文件进行遍历时,顺序和文件顺序一致,这一点和 passwd 文件结论一样,同样的,使用一个书上的一个例子稍加改进进行试验:
  1. #include <shadow.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <errno.h>
  6. #include "../apue.h"
  7. struct spwd* my_getspnam (char const* name)
  8. {
  9.   struct spwd *ptr = 0;
  10.   setspent ();
  11.   while ((ptr = getspent ()) != NULL)
  12.   {
  13.     printf ("%s\t%s\t%ld\t%ld\t%ld\t%ld\t%ld\t%ld\n",
  14.             ptr->sp_namp, ptr->sp_pwdp, ptr->sp_lstchg,
  15.             ptr->sp_min, ptr->sp_max, ptr->sp_warn,
  16.             ptr->sp_inact, ptr->sp_expire);
  17.       
  18.     if (strcmp (name, ptr->sp_namp) == 0)
  19.       break;
  20.   }
  21.   endpwent ();
  22.   return (ptr);
  23. }
  24. int main(int argc, char *argv[])
  25. {
  26.      struct spwd pwd;
  27.      struct spwd *result;
  28.      if (argc != 2) {
  29.           fprintf(stderr, "Usage: %s username\n", argv[0]);
  30.           exit(EXIT_FAILURE);
  31.       }
  32.       result = my_getspnam(argv[1]);
  33.       if (result == NULL) {
  34.           perror("my_getspnam");
  35.           exit(EXIT_FAILURE);
  36.       }
  37.       pwd = *result;
  38.       printf("Name: %s; Pwd: %s\n", pwd.sp_namp, pwd.sp_pwdp);
  39.       exit(EXIT_SUCCESS);
  40. }
复制代码
这个例子演示了如何使用遍历接口模拟 getspnam 的,这里主要的修改是在 my_getspnam 中增加了对遍历信息的输出,这样当给一个不存在的用户名后,就可以把整个文件过一遍啦: 
查看代码
  1.  > sudo ./getspnam_ent abc
  2. root        $6$hT9cNMJc$Ej4tEC3hSHv4jepws0wDgXbIO6lK6GOJ4Yzm1iECfKiq9Bl.zeoNCzr.bI7I3NhPnBezZTK51clj5LuzyXDXc1        18717        0        99999        7        -1        -1
  3. bin        *        17632        0        99999        7        -1        -1
  4. daemon        *        17632        0        99999        7        -1        -1
  5. adm        *        17632        0        99999        7        -1        -1
  6. lp        *        17632        0        99999        7        -1        -1
  7. sync        *        17632        0        99999        7        -1        -1
  8. shutdown        *        17632        0        99999        7        -1        -1
  9. halt        *        17632        0        99999        7        -1        -1
  10. mail        *        17632        0        99999        7        -1        -1
  11. operator        *        17632        0        99999        7        -1        -1
  12. games        *        17632        0        99999        7        -1        -1
  13. ftp        *        17632        0        99999        7        -1        -1
  14. nobody        *        17632        0        99999        7        -1        -1
  15. systemd-network        !!        17850        -1        -1        -1        -1        -1
  16. dbus        !!        17850        -1        -1        -1        -1        -1
  17. polkitd        !!        17850        -1        -1        -1        -1        -1
  18. libstoragemgmt        !!        17850        -1        -1        -1        -1        -1
  19. abrt        !!        17850        -1        -1        -1        -1        -1
  20. rpc        !!        17850        0        99999        7        -1        -1
  21. sshd        !!        17850        -1        -1        -1        -1        -1
  22. postfix        !!        17850        -1        -1        -1        -1        -1
  23. ntp        !!        17850        -1        -1        -1        -1        -1
  24. chrony        !!        17850        -1        -1        -1        -1        -1
  25. tcpdump        !!        17850        -1        -1        -1        -1        -1
  26. work        $6$NHiZrcs5$igsfZKouoJNEYJMezMfG.sDQYA4Xt6Nu1jEkfz/7/C1qs96aXiAsgRJoeYBo7fAf4oeUkV8T3424ZQ4RIrOix0        18058        1        99999        7        -1        -1
  27. centos        !!        18108        1        99999        7        -1        -1
  28. mayun                19295        1        99999        7        -1        -1
  29. zhaomingfu        !!        12000        0        999999        7        -1        -1
  30. jiangze        !!        12000        0        999999        7        -1        -1
  31. shifanjie        !!        12000        0        999999        7        -1        -1
  32. yangmoda        !!        12000        0        999999        7        -1        -1
  33. zhuxiaoxi        !!        12000        0        999999        7        -1        -1
  34. xulei26        !!        12000        0        999999        7        -1        -1
  35. wangzishuo        !!        12000        0        999999        7        -1        -1
  36. yuehongda        !!        12000        0        999999        7        -1        -1
  37. yueguangbin        !!        12000        0        999999        7        -1        -1
  38. lifengjie        !!        12000        0        999999        7        -1        -1
  39. yugeyang        !!        12000        0        999999        7        -1        -1
  40. wangming04        !!        12000        0        999999        7        -1        -1
  41. houhuikun        !!        12000        0        999999        7        -1        -1
  42. liuxinran01        !!        12000        0        999999        7        -1        -1
  43. hanzecheng        !!        12000        0        999999        7        -1        -1
  44. yanghongjun        !!        12000        0        999999        7        -1        -1
  45. lizheyuan        !!        12000        0        999999        7        -1        -1
  46. zhanyongdong        !!        12000        0        999999        7        -1        -1
  47. huxiaoran01        !!        12000        0        999999        7        -1        -1
  48. liuchenghui01        !!        12000        0        999999        7        -1        -1
  49. yunhai01        !!        12000        0        999999        7        -1        -1
  50. liyanan14        !!        12000        0        999999        7        -1        -1
  51. suoning        !!        12000        0        999999        7        -1        -1
  52. panchenglong        !!        12000        0        999999        7        -1        -1
  53. shenhuiyang        !!        12000        0        999999        7        -1        -1
  54. donghan        !!        12000        0        999999        7        -1        -1
  55. chenyun05        !!        12000        0        999999        7        -1        -1
  56. xianghao01        !!        12000        0        999999        7        -1        -1
  57. zhouqi03        !!        12000        0        999999        7        -1        -1
  58. mengzhe        !!        12000        0        999999        7        -1        -1
  59. zhaokexin04        !!        12000        0        999999        7        -1        -1
  60. liuchao15        !!        12000        0        999999        7        -1        -1
  61. niukanglong        !!        12000        0        999999        7        -1        -1
  62. zhengyongpan        !!        12000        0        999999        7        -1        -1
  63. wangjunhan        !!        12000        0        999999        7        -1        -1
  64. shiyiyu        !!        12000        0        999999        7        -1        -1
  65. liuguangming        !!        12000        0        999999        7        -1        -1
  66. piaoxiaoyu        !!        12000        0        999999        7        -1        -1
  67. guochuanlei        !!        12000        0        999999        7        -1        -1
  68. hulingxuan        !!        12000        0        999999        7        -1        -1
  69. ranyunchao        !!        12000        0        999999        7        -1        -1
  70. liushuai06        !!        12000        0        999999        7        -1        -1
  71. lulintong        !!        12000        0        999999        7        -1        -1
  72. songpeipei        !!        12000        0        999999        7        -1        -1
  73. guanzhicheng02        !!        12000        0        999999        7        -1        -1
  74. yuanxueran        !!        12000        0        999999        7        -1        -1
  75. liqilin01        !!        12000        0        999999        7        -1        -1
  76. lirui04        !!        12000        0        999999        7        -1        -1
  77. gaocongcong        !!        12000        0        999999        7        -1        -1
  78. jiahongpeng        !!        12000        0        999999        7        -1        -1
  79. wangyuanyuan14        !!        12000        0        999999        7        -1        -1
  80. chezhuo        !!        12000        0        999999        7        -1        -1
  81. huangfengzhi        !!        12000        0        999999        7        -1        -1
  82. yanxin08        !!        12000        0        999999        7        -1        -1
  83. tanrenzong        !!        12000        0        999999        7        -1        -1
  84. pankai01        !!        12000        0        999999        7        -1        -1
  85. wuyinping        !!        12000        0        999999        7        -1        -1
  86. my_getspnam: No such file or directory
复制代码
观察到几点现象:

  • sp_pwdp 中的 * / !! 保留原样输出
  • 文件中空的 sp_inact / sp_expire 字段变为了 -1
  • 输出比 shadow 文件中的要多,考虑是 NIS 服务提供的网络用户信息
特别是最后一点,当不使用 sudo 提权时,不同机器表现不一致,有的无法从 shadow 文件中获取信息,只能获取 NIS 服务提供的这部分;有的直接失败返回 EACCESS。
一个崩溃

这个代码是复制上一个例子的,复制后无意间少改了一个地方,导致程序一启动就崩溃:
  1. > git diff
  2. diff --git a/06.chapter/getspnam_ent.c b/06.chapter/getspnam_ent.c
  3. index c7021ff..903f96d 100644
  4. --- a/06.chapter/getspnam_ent.c
  5. +++ b/06.chapter/getspnam_ent.c
  6. @@ -11,8 +11,9 @@ my_getspnam (char const* name)
  7. {
  8.    struct spwd *ptr = 0;
  9.    setspent ();
  10. -  while ((ptr = getpwent ()) != NULL)
  11. +  while ((ptr = getspent ()) != NULL)
  12.    {
  13.      if (strcmp (name, ptr->sp_namp) == 0)
  14.        break;
  15.    }
复制代码
原来是将 getpwent 返回的 struct passwd* 强转成了 struct spwd*,之后访问成员导致崩溃,可是这里并没有 (struct spwd*) 强转操作,C 语言不应该报个编译错?
  1. > make
  2. gcc -Wall -g -c getspnam_ent.c -o getspnam_ent.o
  3. getspnam_ent.c: In function ‘my_getspnam’:
  4. getspnam_ent.c:14:3: warning: implicit declaration of function ‘getpwent’ [-Wimplicit-function-declaration]
  5.    while ((ptr = getpwent ()) != NULL)
  6.    ^
  7. getspnam_ent.c:14:15: warning: assignment makes pointer from integer without a cast [enabled by default]
  8.    while ((ptr = getpwent ()) != NULL)
  9.                ^
  10. gcc -Wall -g getspnam_ent.o apue.o -o getspnam_ent
复制代码
看起来像是因为没有包含  从而不识别 getpwent,将它的返回值推断为 int 了,但那也转不到 struct spwd*,而且即使包含了这个头文件也仍然是个 warning,谜之 C  语言……
最终破案了,原来是没有把 apue.h 放在最前面,里有一句定义至关重要:
  1. #define _XOPEN_SOURCE 600 /* Single Unix Specification, Version 3 */
复制代码
在 XSI 扩展中定义的接口必需定义上面的版本号才可以使用:
  1. #if defined __USE_SVID || defined __USE_MISC || defined __USE_XOPEN_EXTENDED
  2. /* Rewind the password-file stream.
  3.    This function is a possible cancellation point and therefore not
  4.    marked with __THROW.  */
  5. extern void setpwent (void);
  6. /* Close the password-file stream.
  7.    This function is a possible cancellation point and therefore not
  8.    marked with __THROW.  */
  9. extern void endpwent (void);
  10. /* Read an entry from the password-file stream, opening it if necessary.
  11.    This function is a possible cancellation point and therefore not
  12.    marked with __THROW.  */
  13. extern struct passwd *getpwent (void);
  14. #endif
复制代码
组文件

在 CentOS 上 struct group 的定义位于  文件中:
  1. /* The group structure.         */
  2. struct group
  3. {
  4.     char *gr_name;                /* Group name.        */
  5.     char *gr_passwd;                /* Password.        */
  6.     __gid_t gr_gid;                /* Group ID.        */
  7.     char **gr_mem;                /* Member list.        */
  8. };
复制代码
POSIX.1 标准定义了上面全部 4 个字段,下面做个简单说明:

  • gr_name 是组名,可通过  getgrname 查找组信息
  • gr_passwd 是组密码,可通过 gpasswd 修改删除组的密码;和 struct passwd 一样,密码不直接保存在这个文件,而是存放于 shadow 文件:/etc/gshadow;当然这是非标准的部分,并不是所有平台都支持
  • gr_gid 是组的唯一 id,可通过 getgrgid 查找组信息
  • gr_mem 是一个指针数组,可以保存多个属于该组的用户名,以 NULL结尾。
这些字段和数据文件中的字段是一一对应的,在 CentOS 上有以下的文件内容:
  1. > cat /etc/group
  2. root:x:0:
  3. bin:x:1:
  4. daemon:x:2:
  5. sys:x:3:
  6. adm:x:4:
  7. tty:x:5:
  8. disk:x:6:
  9. lp:x:7:
  10. mem:x:8:
  11. kmem:x:9:
  12. wheel:x:10:
  13. cdrom:x:11:
  14. mail:x:12:postfix
  15. man:x:15:
  16. dialout:x:18:
  17. floppy:x:19:
  18. games:x:20:
  19. tape:x:33:
  20. video:x:39:
  21. ftp:x:50:
  22. lock:x:54:
  23. audio:x:63:
  24. nobody:x:99:
  25. users:x:100:
  26. utmp:x:22:
  27. utempter:x:35:
  28. input:x:999:
  29. systemd-journal:x:190:
  30. systemd-network:x:192:
  31. dbus:x:81:
  32. polkitd:x:998:
  33. libstoragemgmt:x:997:
  34. ssh_keys:x:996:
  35. abrt:x:173:
  36. rpc:x:32:
  37. sshd:x:74:
  38. slocate:x:21:
  39. postdrop:x:90:
  40. postfix:x:89:
  41. ntp:x:38:
  42. chrony:x:995:
  43. tcpdump:x:72:
  44. stapusr:x:156:
  45. stapsys:x:157:
  46. stapdev:x:158:
  47. yunhai01:x:1000:
  48. cgred:x:994:
  49. mayun:x:1001:
复制代码
字段以冒号分隔,分别对应着 gr_name / gr_passwd / gr_gid / gr_mem 字段,其中:

  • 组密码一直保持 'x'
  • 组成员为空表示只包含和组名同名的用户,当一个用户属于多个组时,这里就会有非空信息了,例如上面的 postfix 用户,下面讲到附加组时还会举更多的例子
遍历顺序

使用 setgrent / getgrent / endgrend 遍历组文件时,顺序和文件顺序一致,这一点和 passwd 文件结论一样,同样的,使用一个书上的一个例子稍加改进进行验证:
  1. #include "../apue.h"
  2. #include <grp.h>
  3. #include <sys/types.h>
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6. #include <unistd.h>
  7. #include <errno.h>
  8. struct group* my_getgrnam (char const* name)
  9. {
  10.   struct group *ptr = 0;
  11.   setgrent ();
  12.   while ((ptr = getgrent ()) != NULL)
  13.   {
  14.     if (strcmp (name, ptr->gr_name) == 0)
  15.       break;
  16.     printf ("%s\n", ptr->gr_name);
  17.   }
  18.   endgrent ();
  19.   return (ptr);
  20. }
  21. int main(int argc, char *argv[])
  22. {
  23.      struct group grp;
  24.      struct group *result;
  25.      if (argc != 2) {
  26.           fprintf(stderr, "Usage: %s group\n", argv[0]);
  27.           exit(EXIT_FAILURE);
  28.       }
  29.       result = my_getgrnam(argv[1]);
  30.       if (result == NULL) {
  31.           perror("getgrnam");
  32.           exit(EXIT_FAILURE);
  33.       }
  34.       grp = *result;
  35.       printf("Name: %s; GID: %d\n", grp.gr_name, grp.gr_gid);
  36.       for (int i=0; grp.gr_mem[i] != 0; ++i)
  37.         printf ("  %s\n", grp.gr_mem[i]);
  38.       exit(EXIT_SUCCESS);
  39. }
复制代码
这个例子演示了如何使用遍历接口模拟 getgrnam 的,这里主要的修改是在 my_getgrnam 中增加了对遍历信息的输出,这样当给一个不存在的用户名后,就可以把整个文件过一遍啦: 
  1. > ./getgrnam_ent abc
  2. root
  3. bin
  4. daemon
  5. sys
  6. adm
  7. tty
  8. disk
  9. lp
  10. mem
  11. kmem
  12. wheel
  13. cdrom
  14. mail
  15. man
  16. dialout
  17. floppy
  18. games
  19. tape
  20. video
  21. ftp
  22. lock
  23. audio
  24. nobody
  25. users
  26. utmp
  27. utempter
  28. input
  29. systemd-journal
  30. systemd-network
  31. dbus
  32. polkitd
  33. libstoragemgmt
  34. ssh_keys
  35. abrt
  36. rpc
  37. sshd
  38. slocate
  39. postdrop
  40. postfix
  41. ntp
  42. chrony
  43. tcpdump
  44. stapusr
  45. stapsys
  46. stapdev
  47. work
  48. nogroup
  49. cgred
  50. centos
  51. mayun
  52. DOORGOD
  53. getgrnam: Success
复制代码
相比 /etc/group 文件,多了 NIS 返回的部分数据:
  1. > paste /etc/group group.txt
  2. root:x:0:        root
  3. bin:x:1:        bin
  4. daemon:x:2:        daemon
  5. sys:x:3:        sys
  6. adm:x:4:centos        adm
  7. tty:x:5:        tty
  8. disk:x:6:        disk
  9. lp:x:7:        lp
  10. mem:x:8:        mem
  11. kmem:x:9:        kmem
  12. wheel:x:10:centos        wheel
  13. cdrom:x:11:        cdrom
  14. mail:x:12:postfix        mail
  15. man:x:15:        man
  16. dialout:x:18:        dialout
  17. floppy:x:19:        floppy
  18. games:x:20:        games
  19. tape:x:33:        tape
  20. video:x:39:        video
  21. ftp:x:50:        ftp
  22. lock:x:54:        lock
  23. audio:x:63:        audio
  24. nobody:x:99:        nobody
  25. users:x:100:        users
  26. utmp:x:22:        utmp
  27. utempter:x:35:        utempter
  28. input:x:999:        input
  29. systemd-journal:x:190:centos        systemd-journal
  30. systemd-network:x:192:        systemd-network
  31. dbus:x:81:        dbus
  32. polkitd:x:998:        polkitd
  33. libstoragemgmt:x:997:        libstoragemgmt
  34. ssh_keys:x:996:        ssh_keys
  35. abrt:x:173:        abrt
  36. rpc:x:32:        rpc
  37. sshd:x:74:        sshd
  38. slocate:x:21:        slocate
  39. postdrop:x:90:        postdrop
  40. postfix:x:89:        postfix
  41. ntp:x:38:        ntp
  42. chrony:x:995:        chrony
  43. tcpdump:x:72:        tcpdump
  44. stapusr:x:156:        stapusr
  45. stapsys:x:157:        stapsys
  46. stapdev:x:158:        stapdev
  47. work:x:1000:        work
  48. nogroup:x:1001:        nogroup
  49. cgred:x:994:        cgred
  50. centos:x:1002:        centos
  51. mayun:x:1003:        mayun
  52.         DOORGOD
复制代码
其中 DOORGOD 即是 NIS 提供的,由 NIS 提供的用户都在这个组中:
  1. > ls -lhrt
  2. total 132K
  3. -rw-rw-r-- 1 yunhai01 DOORGOD 1.4K May 15  2021 getgrnam.c
  4. -rw-rw-r-- 1 yunhai01 DOORGOD  11K May 15  2021 wtmp2.txt
  5. -rw-rw-r-- 1 yunhai01 DOORGOD  174 May 15  2021 utmp.c
  6. -rw-rw-r-- 1 yunhai01 DOORGOD  566 May 15  2021 uname.c
  7. -rw-rw-r-- 1 yunhai01 DOORGOD 1.6K May 15  2021 timeshift.c
  8. -rw-rw-r-- 1 yunhai01 DOORGOD 1.8K May 15  2021 timeprintf.c
  9. -rw-rw-r-- 1 yunhai01 DOORGOD  958 May 15  2021 time.c.org
  10. -rw-rw-r-- 1 yunhai01 DOORGOD  958 May 15  2021 time.c
  11. -rw-rw-r-- 1 yunhai01 DOORGOD 1.3K May 15  2021 setgrps.c
  12. -rw-rw-r-- 1 yunhai01 DOORGOD  15K May 15  2021 ls.out
  13. -rw-rw-r-- 1 yunhai01 DOORGOD  339 May 15  2021 hostname.c
  14. -rw-rw-r-- 1 yunhai01 DOORGOD 1.3K May 15  2021 getspnam.c
  15. -rw-rw-r-- 1 yunhai01 DOORGOD 1.2K May 15  2021 getservnam_ent.c
  16. -rw-rw-r-- 1 yunhai01 DOORGOD  841 May 15  2021 getservnam.c
  17. -rw-rw-r-- 1 yunhai01 DOORGOD 1.2K May 15  2021 getpwnam.c
  18. -rw-rw-r-- 1 yunhai01 DOORGOD 1.1K May 15  2021 getprotonam_ent.c
  19. -rw-rw-r-- 1 yunhai01 DOORGOD  800 May 15  2021 getprotonam.c
  20. -rw-rw-r-- 1 yunhai01 DOORGOD  988 May 15  2021 getnetnam_ent.c
  21. -rw-rw-r-- 1 yunhai01 DOORGOD  906 May 15  2021 getnetnam.c
  22. -rw-rw-r-- 1 yunhai01 DOORGOD 1.1K May 15  2021 gethostnam_ent.c
  23. -rw-rw-r-- 1 yunhai01 DOORGOD  992 May 15  2021 gethostnam.c
  24. -rw-rw-r-- 1 yunhai01 DOORGOD 1.1K May 15  2021 getgrps.c
  25. -rw-r--r-- 1 yunhai01 DOORGOD  342 Nov 13 00:08 shadow.sh
  26. -rw-rw-r-- 1 yunhai01 DOORGOD  974 Nov 13 00:49 getspnam_ent.c
  27. -rw-r--r-- 1 yunhai01 DOORGOD  868 Nov 27 16:34 getpwnam_ent.c
  28. -rw-rw-r-- 1 yunhai01 DOORGOD  945 Nov 27 16:34 getgrnam_ent.c
  29. -rw-rw-r-- 1 yunhai01 DOORGOD 3.3K Nov 27 16:35 Makefile
  30. -rw-r--r-- 1 yunhai01 DOORGOD  337 Nov 27 16:48 group.txt
复制代码
附加组

早期 unix 系统中,一个用户只能属于一个组,当临时需要借用另一组权限时,使用 newgrp {group} 命令切换,完成后再使用无参数的 newgrp 返回。如果新的组有密码,需要输入匹配的密码才可以加入。后面随着系统的发展,引入了附加组的概念,一个用户除了属于一个主组 (initial group) 外,还可以属于最多不超过 NGROUPS_MAX (65536 CentOS) 个附加组,相应的文件权限检查时,除了将进程有效组 ID 与主组 ID 进行比较外,还与所有附加组 ID 进行比较,只有有一个能匹配上,就可以通过权限检查。这样一来就避免了频繁的切换组。关于文件权限的内容,可以参考我之前写的这篇:《[apue] linux 文件系统那些事儿》。
在开始用例子说明添加用户到组之前,先熟悉下与用户和用户组相关的几个命令:

  • useradd / userdel / usermod 是用户的增删改;
  • groupadd / groupdel / groupmod 是用户组的增删改;
  • passwd / gpaaswd 分别是用户和组密码的增删改。
其中:

  • useradd / usermod 都可以通过 -g 参数指定主组、-G 参数指定附加组,多个组名之前以逗号分隔
  • usermod -G 指定的附加组列表会直接替换用户的附加组,如果仅添加,需要指定 -a 选项。对于删除,usermod 比较无力,需要得到用户之前的所有附加组,去掉想删除的组后直接使用 -G 设置
  • 除了 usermod 从用户角度出发,gpasswd 从用户组的角度出发也可以修改组包含的用户列表,主要是通过  -a 选项添加用户,-d 选项删除用户,-M 选项直接设置组的所有用户。对比下来,想删除某个用户的附加组,使用 gpasswd -d 更方便一些
下面演示为 mayun 账户添加多个附加组:
  1. > sudo usermod -a -G centos,sshd,work,ntp,dbus,games,ftp,man mayun
  2. > sudo gpasswd -M centos,sshd,work,ntp,dbus,games,ftp,daemon mayun
  3. > id mayun
  4. uid=1002(mayun) gid=1003(mayun) groups=1003(mayun),15(man),20(games),50(ftp),81(dbus),74(sshd),38(ntp),1000(work),1002(centos)
  5. > cat /etc/group
  6. root:x:0:
  7. bin:x:1:
  8. daemon:x:2:
  9. sys:x:3:
  10. adm:x:4:centos
  11. tty:x:5:
  12. disk:x:6:
  13. lp:x:7:
  14. mem:x:8:
  15. kmem:x:9:
  16. wheel:x:10:centos
  17. cdrom:x:11:
  18. mail:x:12:postfix
  19. man:x:15:mayun
  20. dialout:x:18:
  21. floppy:x:19:
  22. games:x:20:mayun
  23. tape:x:33:
  24. video:x:39:
  25. ftp:x:50:mayun
  26. lock:x:54:
  27. audio:x:63:
  28. nobody:x:99:
  29. users:x:100:
  30. utmp:x:22:
  31. utempter:x:35:
  32. input:x:999:
  33. systemd-journal:x:190:centos
  34. systemd-network:x:192:
  35. dbus:x:81:mayun
  36. polkitd:x:998:
  37. libstoragemgmt:x:997:
  38. ssh_keys:x:996:
  39. abrt:x:173:
  40. rpc:x:32:
  41. sshd:x:74:mayun
  42. slocate:x:21:
  43. postdrop:x:90:
  44. postfix:x:89:
  45. ntp:x:38:mayun
  46. chrony:x:995:
  47. tcpdump:x:72:
  48. stapusr:x:156:
  49. stapsys:x:157:
  50. stapdev:x:158:
  51. work:x:1000:mayun
  52. nogroup:x:1001:
  53. cgred:x:994:
  54. centos:x:1002:mayun
  55. mayun:x:1003:centos,sshd,work,ntp,dbus,games,ftp,daemon
  56. > sudo usermod -G mayun mayun
  57. > id mayun
  58. uid=1002(mayun) gid=1003(mayun) groups=1003(mayun)
  59. > sudo gpasswd -M mayun mayun
复制代码
脚本使用 usermod 为用户 mayun 添加附加组,使用 gpasswd 为用户组 mayun 添加用户,通过 id 展示用户所属组信息,也通过查看 /etc/group 验证了这一点,最后恢复原状。
典型用例

关于附加组有如下几个 api:
  1. int getgroups(int gidsetsize, gid_t grouplist[]);
  2. int setgroups(int ngroups, const gid_t gidlist[]);
  3. int initgroups(const char *name, gid_t basegid);
复制代码
下面结合使用场景对他们做个简单说明:

  • getgropus 随时可以调用,gidsetsize 应与 grouplist 维度匹配,如果 gidsetsize = 0,则返回 grouplist 的维度,以便用户分配存储空间接收它们
  • 只有超级用户可以调用 setgroups 来为调用进程设置附加组 ID 列表,ngroups 不能大于 NGROUPS_MAX
  • 只有超级用户可以调用 initgroups 来初始化账户的附加组列表,它通过 setgrent/getgrent/endgrent 读取组文件,遍历其中包含成员为 name 的组,然后调用 setgroups 设置这它们,此外还会设置 basegid 作为初始组,它是 name 在口令文件中的对应的组 ID,用以区分组 ID 和附加组 ID
  • login 进程会在用户登录时调用 initgroups
主机

在 CentOS 上 hostent 结构体的定义位于  文件中:
  1. struct hostent
  2. {
  3.   char *h_name;                        /* Official name of host.  */
  4.   char **h_aliases;                /* Alias list.  */
  5.   int h_addrtype;                /* Host address type.  */
  6.   int h_length;                        /* Length of address.  */
  7.   char **h_addr_list;                /* List of addresses from name server.  */
  8. };
复制代码
其中:

  • h_name 表示主机名,这通常是用域名表示的,如 baidu.com
  • h_addrtype 一般为 AF_INET 或 AF_INET6
  • h_addr_list 用来存放多个地址指针,以 NULL 结尾。为了向后兼容,通常将 h_addr 定义为链表中第一个元素
/etc/hosts 文件一般只有很少的内容,除非明确指定域名到 IP 的映射,一般不更改这个文件,我的 CentOS 上它有以下内容:
  1. 127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
  2. ::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
  3. 10.9.225.242 goodcitizen.bcc-gzhxy.baidu.com goodcitizen.bcc-gzhxy.baidu.com
  4. 140.82.114.3 github.com
  5. 140.82.114.10 nodeload.github.com
  6. 140.82.114.6 api.github.com
  7. 140.82.114.10 codeload.github.com
  8. 203.208.39.193 dl.google.com
复制代码
第一列是 IP 地址,第二列是域名。可以看到为了增加国内 github 的解析我增加了一些内容,这样 ping github.com 时将直接使用指定的 IP 进行连接。
通过 sethostent/gethostent/endhostent 遍历的信息将仅限文件内容,而 gethostbyname/gethostbyaddr 则可以返回任意合法域名的地址,它的取值范围远远大于 /etc/hosts 的范围,这是和其它 api 最大的不同点。下面的这个程序演示了这一点,首先验证文件遍历的方式:
  1. #include <netdb.h>
  2. #include <sys/socket.h>
  3. #include <netinet/in.h>
  4. #include <arpa/inet.h>
  5. #include <stdio.h>
  6. #include <stdlib.h>
  7. #include <unistd.h>
  8. #include <errno.h>
  9. #include "../apue.h"
  10. struct hostent* my_gethostnam (char const* name)
  11. {
  12.   struct hostent *ptr = 0;
  13.   sethostent (1);
  14.   while ((ptr = gethostent ()) != NULL)
  15.   {
  16.     printf ("searching %s\n", ptr->h_name);
  17.     if (strcmp (name, ptr->h_name) == 0)
  18.       break;
  19.   }
  20.   endhostent ();
  21.   return (ptr);
  22. }
  23. int
  24. main(int argc, char *argv[])
  25. {
  26.      struct hostent *result;
  27.      if (argc != 2) {
  28.           fprintf(stderr, "Usage: %s hostname\n", argv[0]);
  29.           exit(EXIT_FAILURE);
  30.       }
  31.       result = my_gethostnam(argv[1]);
  32.       if (result == NULL) {
  33.           perror("gethostnam");
  34.           exit(EXIT_FAILURE);
  35.       }
  36.       printf("Name: %s; type: %d\n", result->h_name, result->h_addrtype);
  37.       int i = 0;
  38.       char **p = result->h_addr_list;
  39.       while (p && p[i])
  40.       {
  41.         printf("  %s\n", inet_ntoa(*(struct in_addr*)p[i]));
  42.         i++;
  43.       }
  44.       exit(EXIT_SUCCESS);
  45. }
复制代码
这个例子演示了如何使用遍历接口模拟 gethostbynam 的,这里主要的修改是在 my_gethostnam 中增加了对遍历信息的输出,这样当给一个不存在的域名后,就可以把整个文件过一遍啦: 
  1. > ./gethostnam_ent baidu.com
  2. searching localhost
  3. searching localhost
  4. searching goodcitizen.bcc-gzhxy.baidu.com
  5. searching github.com
  6. searching nodeload.github.com
  7. searching api.github.com
  8. searching codeload.github.com
  9. searching dl.google.com
  10. gethostnam: Success
复制代码
可以看到因为给定的域名不在 hosts 文件中,所以即使是合法的域名最后也没有找到。如果将这里的 my_gethostbynam 替换为标准的 gethostbynam,结果就大不相同了:
  1. > ./gethostnam baidu.com
  2. Name: baidu.com; type: 2
  3.   220.181.38.251
  4.   220.181.38.148
复制代码
如果给定的域名是 hosts 中已经存在的,则不管是否有网络都可以得到结果:
  1. > ./gethostnam github.com
  2. Name: github.com; type: 2
  3.   140.82.114.3
复制代码
因此可以这样理解,hosts 仅仅是在系统自动解析域名的基础上增加了自定义域名映射的功能,而且具有更高优先级。另外遍历的时候只返回文件中的内容也好理解,如果将网络上的 DNS 信息遍历一遍,那绝对是一件不可能完成的任务,也没有必要。gethostbynam 对于没有 DNS 缓存的域名,也是通过在网络上发送 DNS 请求来实现的,所以当网络不通时,这个接口也无法正常工作了。
uname

上面的内容主要是获取网络主机名地址的,那如何获取本地主机名呢?POSIX 提供了两个接口,首先来看 uname:
  1. /* Structure describing the system and machine.  */
  2. struct utsname
  3. {
  4.     /* Name of the implementation of the operating system.  */
  5.     char sysname[_UTSNAME_SYSNAME_LENGTH];
  6.     /* Name of this node on the network.  */
  7.     char nodename[_UTSNAME_NODENAME_LENGTH];
  8.     /* Current release level of this implementation.  */
  9.     char release[_UTSNAME_RELEASE_LENGTH];
  10.     /* Current version level of this release.  */
  11.     char version[_UTSNAME_VERSION_LENGTH];
  12.     /* Name of the hardware type the system is running on.  */
  13.     char machine[_UTSNAME_MACHINE_LENGTH];
  14. };
  15. int uname(struct utsname *name);
复制代码
uname 返回 utsname 结构体,分别包含了系统名称、主机名称、发布名称、版本、机器类型,下面是在 CentOS 上调用的输出:
  1. > ./uname
  2. sizeof (struct utsname) = 390
  3. sysname: Linux
  4. nodename: goodcitizen.bcc-gzhxy.baidu.com
  5. release: 3.10.0-1160.80.1.el7.x86_64
  6. version: #1 SMP Tue Nov 8 15:48:59 UTC 2022
  7. machine: x86_64
复制代码
系统命令 uname 可以直接输出这些信息:
  1. > uname -s
  2. Linux
  3. > uname -n
  4. goodcitizen.bcc-gzhxy.baidu.com
  5. > uname -r
  6. 3.10.0-1160.80.1.el7.x86_64
  7. > uname -v
  8. #1 SMP Tue Nov 8 15:48:59 UTC 2022
  9. > uname -m
  10. x86_64
  11. > uname -a
  12. Linux goodcitizen.bcc-gzhxy.baidu.com 3.10.0-1160.80.1.el7.x86_64 #1 SMP Tue Nov 8 15:48:59 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
复制代码
可以看到示例中各选项与字段的对应关系。
gethostname
  1. int gethostname(char *name, size_t namelen);
  2. int sethostname(const char *name, int namelen);
复制代码
gethostname 只输出主机名称,看源码它直接调用 uname 并返回 nodename 字段,名称长度限制为 HOST_NAME_MAX (CentOS 64)。 sethostname 则只有超级用户可以调用,通常在系统自举时设置,由 /etc/rc 或 init 取自一个启动文件。
网络

在 CentOS 上 netent 结构体位于  文件中:
  1. struct netent {
  2.     char      *n_name;     /* official network name */
  3.     char     **n_aliases;  /* alias list */
  4.     int        n_addrtype; /* net address type */
  5.     uint32_t   n_net;      /* network number */
  6. }
复制代码
对应的文件是 /etc/networks,在 CentOS 上只找到寥寥几条记录:
  1. default 0.0.0.0
  2. loopback 127.0.0.0
  3. link-local 169.254.0.0
复制代码
关于 getnetbyname 及 getnetbyaddr,一直没明白有什么用处,所以这节就简单带过了,用法和上一节别无二致。
协议

在 CentOS 上 protoent 结构体位于  文件中:
  1. struct protoent {
  2.     char  *p_name;       /* official protocol name */
  3.     char **p_aliases;    /* alias list */
  4.     int    p_proto;      /* protocol number */
  5. }
复制代码
其中:

  • p_name 是协议名,如 icmp、tcp、ip
  • p_proto 是协议号,对应着 IPPROTO_XXX 的定义,例如 IPPROTO_ICMP = 1,IPPROTO_TCP = 6, IPPROTO_IP = 0
/etc/protocols 包含了所有的协议,内容比较多,这里就不贴整个文件了,取一些典型的数据列出来:
  1. > cat /etc/protocols
  2. ...
  3. ip        0        IP                # internet protocol, pseudo protocol number
  4. hopopt        0        HOPOPT                # hop-by-hop options for ipv6
  5. icmp        1        ICMP                # internet control message protocol
  6. igmp        2        IGMP                # internet group management protocol
  7. ggp        3        GGP                # gateway-gateway protocol
  8. ipv4        4        IPv4                # IPv4 encapsulation
  9. st        5        ST                # ST datagram mode
  10. tcp        6        TCP                # transmission control protocol
  11. cbt        7        CBT                # CBT, Tony Ballardie <A.Ballardie@cs.ucl.ac.uk>
  12. egp        8        EGP                # exterior gateway protocol
  13. igp        9        IGP                # any private interior gateway (Cisco: for IGRP)
  14. bbn-rcc        10        BBN-RCC-MON                # BBN RCC Monitoring
  15. nvp        11        NVP-II                # Network Voice Protocol
  16. pup        12        PUP                # PARC universal packet protocol
  17. argus        13        ARGUS                # ARGUS
  18. emcon        14        EMCON                # EMCON
  19. xnet        15        XNET                # Cross Net Debugger
  20. chaos        16        CHAOS                # Chaos
  21. udp        17        UDP                # user datagram protocol
  22. mux        18        MUX                # Multiplexing protocol
  23. dcn        19        DCN-MEAS                # DCN Measurement Subsystems
  24. hmp        20        HMP                # host monitoring protocol
  25. prm        21        PRM                # packet radio measurement protocol
  26. xns-idp        22        XNS-IDP                # Xerox NS IDP
  27. trunk-1        23        TRUNK-1                # Trunk-1
  28. trunk-2        24        TRUNK-2                # Trunk-2
  29. leaf-1        25        LEAF-1                # Leaf-1
  30. leaf-2        26        LEAF-2                # Leaf-2
  31. rdp        27        RDP                # "reliable datagram" protocol
  32. irtp        28        IRTP                # Internet Reliable Transaction Protocol
  33. iso-tp4        29        ISO-TP4                # ISO Transport Protocol Class 4
  34. netblt        30        NETBLT                # Bulk Data Transfer Protocol
  35. ...
  36. >  cat /etc/protocols | wc -l
  37. 162
复制代码
第一列是协议名,第二列是协议号,第三列是别名。# 号开头的为注释不是有效记录。
通过 setprotoent/getprotoent/endprotoent 遍历的内容与文件内容完全一致,且顺序一致。这里就不再演示了。
与 /etc/networks 一样,我没找到这些接口的使用场景,一般在编程阶段就要确定使用的协议类型,直接指定头文件中的 IPPROTO_XX 即可,有什么必要通过 getprotobynam 来查询一遍呢?除非是为了某种可拓展性,当不同协议经过抽象后除了协议部分的代码完全一致时,可以通过在配置文件中指定协议名的方式来快速切换底层的实现,那么这时就可以使用 getprotobynam 来查询对应的协议号,这样一看还是有点用的哈~
服务

在 CentOS 上 servent 结构体位于  文件中:
  1. struct servent {
  2.     char  *s_name;       /* official service name */
  3.     char **s_aliases;    /* alias list */
  4.     int    s_port;       /* port number */
  5.     char  *s_proto;      /* protocol to use */
  6. }
复制代码
其中:

  • s_name 表示服务名,如 ssh、http、https、ftp 等
  • s_port 表示连接的端口号,注意字节顺序是网络序,展示前需要转换为主机序
  • s_proto 表示底层传输层协议,如 tcp、udp 等
/etc/services 包含了所有的服务,在 CentOS 上有以下内容 (内容有缩减):
  1. > cat /etc/services | wc -l
  2. 11176
  3. > cat /etc/services
  4. ...
  5. # 21 is registered to ftp, but also used by fsp
  6. ftp             21/tcp
  7. ftp             21/udp          fsp fspd
  8. ssh             22/tcp                          # The Secure Shell (SSH) Protocol
  9. ssh             22/udp                          # The Secure Shell (SSH) Protocol
  10. telnet          23/tcp
  11. telnet          23/udp
  12. # 24 - private mail system
  13. lmtp            24/tcp                          # LMTP Mail Delivery
  14. lmtp            24/udp                          # LMTP Mail Delivery
  15. smtp            25/tcp          mail
  16. smtp            25/udp          mail
  17. time            37/tcp          timserver
  18. time            37/udp          timserver
  19. rlp             39/tcp          resource        # resource location
  20. rlp             39/udp          resource        # resource location
  21. nameserver      42/tcp          name            # IEN 116
  22. nameserver      42/udp          name            # IEN 116
  23. nicname         43/tcp          whois
  24. nicname         43/udp          whois
  25. ...
复制代码
第一列是服务名,第二列是端口号与协议名,通过斜杠分隔。# 号开头的为注释不是有效记录。
通过 setservent/getservent/endservent 遍历的内容与文件内容完全一致,且顺序一致,这里就不再演示了。
getaddrinfo

为了简化 gethostbynam/gethostbyaddr 与 getservbynam/getservbyport 调用,Linux 上推出了一组新的接口:
  1. int getnameinfo(const struct sockaddr *sa, socklen_t salen,
  2.                 char *host, size_t hostlen,
  3.                 char *serv, size_t servlen, int flags);
  4. int getaddrinfo(const char *node, const char *service,
  5.                 const struct addrinfo *hints,
  6.                 struct addrinfo **res);
  7. void freeaddrinfo(struct addrinfo *res);
  8. const char *gai_strerror(int errcode);
复制代码
其中:

  • getnameinfo = gethostbyaddr + getservbyport,根据地址查询主机名与服务名
  • getaddrinfo = gethostbyname + getservbyname,根据主机名与服务名查询地址信息
  • freeaddrinfo 用来释放与地址相关的内存,这块内存由 getaddrinfo 返回
  • gai_strerror 用来解释 getaddrinfo 的返回值
addrinfo 结构体定义如下:
  1. struct addrinfo {
  2.     int              ai_flags;
  3.     int              ai_family;
  4.     int              ai_socktype;
  5.     int              ai_protocol;
  6.     socklen_t        ai_addrlen;
  7.     struct sockaddr *ai_addr;
  8.     char            *ai_canonname;
  9.     struct addrinfo *ai_next;
  10. };
复制代码
不多做解释了,感兴趣的读者可以查看 man 手册页,这里主要关注一下 ai_next 字段,返回的多个地址可以通过这个字段串连成链表,比之前直观了不少。
除了简化用户调用,这组接口最大的好处是可重入性,无需担心静态存储区覆盖的问题,同时也能助力消除 ipv4 与 ipv6 的依赖问题 (allows programs to eliminate IPv4-versus-IPv6 dependencies)。
用户登录

用户登录相关的信息主要存储于 utmp/wtmp/btmp 三个文件中,下面一一说明。
utmp

一般位于 /var/run/utmp,记录当前登录进系统的各个用户。login 程序在用户登录时会填写一条 utmp 记录到该文件,注销时, init 进程将 utmp 文件中相应的记录擦除 (每个字节都填 0),utmp 结构的定义位于  文件中:
  1. #define UT_LINESIZE      32
  2. #define UT_NAMESIZE      32
  3. #define UT_HOSTSIZE     256
  4. struct exit_status {              /* Type for ut_exit, below */
  5.     short int e_termination;      /* Process termination status */
  6.     short int e_exit;             /* Process exit status */
  7. };
  8. struct utmp {
  9.     short   ut_type;              /* Type of record */
  10.     pid_t   ut_pid;               /* PID of login process */
  11.     char    ut_line[UT_LINESIZE]; /* Device name of tty - "/dev/" */
  12.     char    ut_id[4];             /* Terminal name suffix, or inittab(5) ID */
  13.     char    ut_user[UT_NAMESIZE]; /* Username */
  14.     char    ut_host[UT_HOSTSIZE]; /* Hostname for remote login, or kernel version for run-level messages */
  15.     struct  exit_status ut_exit;  /* Exit status of a process marked as DEAD_PROCESS; not used by Linux init(8) */
  16.      long   ut_session;           /* Session ID */
  17.      struct timeval ut_tv;        /* Time entry was made */
  18.     int32_t ut_addr_v6[4];        /* Internet address of remote host; IPv4 address uses just ut_addr_v6[0] */
  19.     char __unused[20];            /* Reserved for future use */
  20. };
  21. /* Backward compatibility hacks */
  22. #define ut_name ut_user
  23. #define ut_time ut_tv.tv_sec
  24. #define ut_addr ut_addr_v6[0]
复制代码
注释基本可以解释各个字段的含义,书上老版本的结构体只介绍了 ut_line / ut_name / ut_time 三个字段,后两个通过 define 定义到了 ut_user 和 ut_tv.tv_sec 字段。另外 64 位的 ut_tv / ut_session 类型会不一样,这里为了简化没有列出完整的定义,感兴趣的可以 man utmp 自行查看。
utmpdump

由于 /var/run/utmp 是二进制的,无法直接查看,想要看这个文件的内容,只能通过 utmpdump 命令转换后查看:
  1. > utmpdump /var/run/utmp
  2. Utmp dump of /var/run/utmp
  3. [2] [00000] [~~  ] [reboot  ] [~           ] [3.10.0-1160.80.1.el7.x86_64] [0.0.0.0        ] [Wed Dec 07 16:22:20 2022 CST]
  4. [1] [00051] [~~  ] [runlevel] [~           ] [3.10.0-1160.80.1.el7.x86_64] [0.0.0.0        ] [Wed Dec 07 16:22:29 2022 CST]
  5. [6] [01321] [tyS0] [LOGIN   ] [ttyS0       ] [                    ] [0.0.0.0        ] [Wed Dec 07 16:22:29 2022 CST]
  6. [6] [01320] [tty1] [LOGIN   ] [tty1        ] [                    ] [0.0.0.0        ] [Wed Dec 07 16:22:29 2022 CST]
  7. [7] [03628] [ts/0] [yunhai01] [pts/0       ] [172.31.43.62        ] [172.31.43.62   ] [Sun Jan 01 11:48:53 2023 CST]
  8. [8] [24901] [ts/1] [        ] [pts/1       ] [                    ] [172.31.23.41   ] [Wed Dec 14 15:18:50 2022 CST]
  9. [8] [04965] [ts/2] [        ] [pts/2       ] [                    ] [172.31.22.20   ] [Wed Dec 21 18:49:00 2022 CST]
  10. [8] [27816] [ts/3] [        ] [pts/3       ] [                    ] [172.31.23.41   ] [Fri Dec 30 18:28:00 2022 CST]
复制代码
其中各个列和字段并不是一一对应的关系,不过关键的进程 ID、用户名、主机名、ip 地址、登录时间还是很好分辨的:

  • 第一列为 ut_type,取值如下 (其中 7 表示当前正在登录):
  1. #define EMPTY         0 /* Record does not contain valid info (formerly known as UT_UNKNOWN on Linux) */
  2. #define RUN_LVL       1 /* Change in system run-level (see init(8)) */
  3. #define BOOT_TIME     2 /* Time of system boot (in ut_tv) */
  4. #define NEW_TIME      3 /* Time after system clock change (in ut_tv) */
  5. #define OLD_TIME      4 /* Time before system clock change (in ut_tv) */
  6. #define INIT_PROCESS  5 /* Process spawned by init(8) */
  7. #define LOGIN_PROCESS 6 /* Session leader process for user login */
  8. #define USER_PROCESS  7 /* Normal process */
  9. #define DEAD_PROCESS  8 /* Terminated process */
  10. #define ACCOUNTING    9 /* Not implemented */
复制代码

  •  第二列是 ut_pid,通过 pstree 可以验证 (其中状态 7 对应的 PID 3628 为 sshd 进程):
  1. > pstree -nph
  2. ...
  3.            ├─sshd(1282)───sshd(3628)───sshd(3751)───bash(3777)───bash(3807)─┬─man(29705)───less(29719)
  4.            │                                                                └─pstree(30238)
  5. ...
复制代码

  • 第三列为 ut_id,有一些例外:

    • 当状态为 RUN_LVL (1:运行级别改变) 或 BOOT_TIME (2:系统重启) 时,为 ~~
    • ttyX 表示终端名。注意因为长度限制,ttyS0 会被截断为 tyS0
    • pts/X 表示伪终端名。同样因为长度限制,pts/0 会被截断为 ts/0

  • 第四列为 ut_user,也有例外:

    • 当状态为 RUN_LVL 时为 runlevel
    • 当状态为 BOOT_TIME 时为 reboot
    • 空表示非活跃用户 (DEAD_PROCESS)

  • 第五列为 ut_line,是 ut_id 的完整版
  • 第六列为 ut_host,有例外:

    • 当状态为 RUN_LVL 或 BOOT_TIME 时为内核版本号
    • 本地登录为空

  • 第七列为 ut_addr,v4 地址仅使用 ut_addr_v6[0] 表示,全零表示本地登录
  • 第八列为 ut_time
各个列具体列含义可参考文末链接。
字段变更

系统启动后首先启动 init 进程,该进程首先会清理 utmp 文件,这主要是通过:

  • 将 ut_type 设置为 DEAD_PROCESS (8)
  • 对于查找不到 ut_pid 信息且并状态不为 DEAD_PROCESS / RUN_LVL 记录,清空其 ut_user / ut_host / ut_time 字段
下面是进程生命周期过程中各个字段的变更逻辑:

  • init 进程根据 inittab 新建进程时如果没有匹配的空记录 (通过 ut_id) 则插入一条新的 utmp 记录,ut_id 设置为 inittab 中对应的字段,并且设置 ut_pid 和 ut_time 字段,最后设置 ut_type 为 INIT_PROCESS (5)。
  • mingettty 或 agetty 进程根据  pid 查找入口设置 ut_line,修改 ut_type 为 LOGIN_PROCESS (6),更新 ut_time 字段
  • login 进程校验用户登录信息后,设置 ut_type 为 USER_PROCESS (7),设置 ut_host 和 ut_addr 字段,更新 ut_time 字段
  • 上面是通过 sshd 伪终端登录的情况,直接通过 xterm 登录 (本地登录) 的情况则简单很多,xterm 直接设置 ut_type 为 USER_PROCESS,ut_id 设置为终端名称的末 4 位。
  • init 或 xterm 进程检测到进程退出后,设置对应记录的 ut_type 为 DEAD_PROCESS,清理 ut_user / ut_host / ut_time 字段 (ut_time 被清理存疑)
遍历内容

通过 setutent/getutent/endutent  可以遍历的 utmp 文件内容,像之前一样,写一个 demo 演示一下:
  1. #include "../apue.h"
  2. #include <utmp.h>
  3. #include <arpa/inet.h>
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6. #include <unistd.h>
  7. #include <errno.h>
  8. struct utmp* my_getutnam (char const* name)
  9. {
  10.   struct utmp *ptr = 0;
  11.   setutent ();
  12.   while ((ptr = getutent ()) != NULL)
  13.   {
  14.     struct in_addr addr = { 0 };
  15.     addr.s_addr = ptr->ut_addr_v6[0];
  16.     printf("type: %d, pid: %u, line: %s, utid: %.4s, user: %s, host: %s, exit: %d, sess: %d, time: %d, addr: %s\n",
  17.             ptr->ut_type, ptr->ut_pid, ptr->ut_line, ptr->ut_id, ptr->ut_user, ptr->ut_host, ptr->ut_exit.e_exit,
  18.             ptr->ut_session, ptr->ut_tv.tv_sec, inet_ntoa(addr));
  19.     if (strcmp (name, ptr->ut_user) == 0)
  20.       break;
  21.   }
  22.   endutent ();
  23.   return (ptr);
  24. }
  25. int main(int argc, char *argv[])
  26. {
  27.      struct utmp tmp;
  28.      struct utmp *result;
  29.      if (argc != 2) {
  30.           fprintf(stderr, "Usage: %s username\n", argv[0]);
  31.           exit(EXIT_FAILURE);
  32.       }
  33.       result = my_getutnam(argv[1]);
  34.       if (result == NULL) {
  35.           perror("getutnam");
  36.           exit(EXIT_FAILURE);
  37.       }
  38.       tmp = *result;
  39.       printf ("find record!\n");
  40.       exit(EXIT_SUCCESS);
  41. }
复制代码
运行 demo 得到如下输出:
  1. $ ./getutnam_ent abc
  2. type: 2, pid: 0, line: ~, utid: ~~, user: reboot, host: 3.10.0-1160.80.1.el7.x86_64, exit: 0, sess: 0, time: 1670401340, addr: 0.0.0.0
  3. type: 1, pid: 51, line: ~, utid: ~~, user: runlevel, host: 3.10.0-1160.80.1.el7.x86_64, exit: 0, sess: 0, time: 1670401349, addr: 0.0.0.0
  4. type: 6, pid: 1321, line: ttyS0, utid: tyS0, user: LOGIN, host: , exit: 0, sess: 1321, time: 1670401349, addr: 0.0.0.0
  5. type: 6, pid: 1320, line: tty1, utid: tty1, user: LOGIN, host: , exit: 0, sess: 1320, time: 1670401349, addr: 0.0.0.0
  6. type: 7, pid: 28957, line: pts/0, utid: ts/0, user: yunhai01, host: 172.31.23.41, exit: 0, sess: 0, time: 1672645341, addr: 172.31.23.41
  7. type: 8, pid: 24901, line: pts/1, utid: ts/1, user: , host: , exit: 0, sess: 0, time: 1671002330, addr: 172.31.23.41
  8. type: 8, pid: 4965, line: pts/2, utid: ts/2, user: , host: , exit: 0, sess: 0, time: 1671619740, addr: 172.31.22.20
  9. type: 8, pid: 27816, line: pts/3, utid: ts/3, user: , host: , exit: 0, sess: 0, time: 1672396080, addr: 172.31.23.41
  10. getutnam: No such file or directory
复制代码
输出与 utmpdump 基本相同,并且验证了 ut_line 字段是完整的 (伪) 终端名称,而 ut_id 只是其最后四位。
典型案例

who 命令的实现依赖 utmp 的信息:
  1. > who
  2. yunhai01 pts/0        2023-01-01 11:48 (172.31.43.62)
复制代码
通过 strace 的信息可以观察到这一点:
  1. > strace who |& grep -E 'open|access'
  2. access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
  3. open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
  4. open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
  5. open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
  6. access("/var/run/utmpx", F_OK)          = -1 ENOENT (No such file or directory)
  7. open("/var/run/utmp", O_RDONLY|O_CLOEXEC) = 3
  8. open("/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
  9. open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
  10. open("/lib64/libnss_files.so.2", O_RDONLY|O_CLOEXEC) = 3
  11. open("/etc/group", O_RDONLY|O_CLOEXEC)  = 3
  12. open("/etc/localtime", O_RDONLY|O_CLOEXEC) = 3
复制代码
重点看 open 调用,有打开 /var/run/utmp 的记录,通过 starce 日志还发现在此之前尝试过 /var/run/utmpx 文件,这个可能是 linux 上衍生出的新的 utmp 文件,不过在我这台 CentOS 上并没有这个文件,所以走的还是老 utmp 文件。
除 who 之外,w 命令也是通过 utmp 命令获取正在登录用户的信息:
  1. > w
  2. 12:42:54 up 24 days, 20:20,  1 user,  load average: 0.04, 0.15, 0.18
  3. USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
  4. yunhai01 pts/0    172.31.43.62     11:48    6.00s  4.12s  0.00s w
复制代码
并且打印了用户当前正在执行的命令及负载信息。通过 starce 也能看到同样的结论:
  1. > strace w |& grep utmp
  2. read(4, "grep\0--color=auto\0utmp\0", 2047) = 23
  3. access("/var/run/utmpx", F_OK)          = -1 ENOENT (No such file or directory)
  4. open("/var/run/utmp", O_RDONLY|O_CLOEXEC) = 4
  5. access("/var/run/utmpx", F_OK)          = -1 ENOENT (No such file or directory)
  6. open("/var/run/utmp", O_RDONLY|O_CLOEXEC) = 5
复制代码
通过日志发现 w 命令一次打开两个 utmp 文件句柄。
wtmp

一般位于 /var/log/wtmp,用于跟踪各个登录和注销事件。login 程序在用户登录时会填写一条记录到该文件,注销时,init 进程将一个新记录添写到 wtmp 文件,其中 ut_name 字段清空。在系统重新启动、更改系统时间和日期的前后,都会在 wtmp 文件中添写特殊的记录项。wtmp 文件中记录的也是 utmp 结构体,因此可以通过 utmpdump 来查看:
  1. > utmpdump /var/log/wtmp
  2. [2] [00000] [~~  ] [reboot  ] [~           ] [3.10.0-1160.80.1.el7.x86_64] [0.0.0.0        ] [Wed Dec 07 15:58:12 2022 CST]
  3. [1] [00051] [~~  ] [runlevel] [~           ] [3.10.0-1160.80.1.el7.x86_64] [0.0.0.0        ] [Wed Dec 07 15:58:21 2022 CST]
  4. [5] [01320] [tyS0] [        ] [ttyS0       ] [                    ] [0.0.0.0        ] [Wed Dec 07 15:58:24 2022 CST]
  5. [5] [01319] [tty1] [        ] [tty1        ] [                    ] [0.0.0.0        ] [Wed Dec 07 15:58:24 2022 CST]
  6. [6] [01319] [tty1] [LOGIN   ] [tty1        ] [                    ] [0.0.0.0        ] [Wed Dec 07 15:58:24 2022 CST]
  7. [6] [01320] [tyS0] [LOGIN   ] [ttyS0       ] [                    ] [0.0.0.0        ] [Wed Dec 07 15:58:24 2022 CST]
  8. [7] [01319] [tty1] [root    ] [tty1        ] [                    ] [0.0.0.0        ] [Wed Dec 07 15:59:52 2022 CST]
  9. [7] [01749] [ts/0] [yunhai01] [pts/0       ] [172.31.43.62        ] [172.31.43.62   ] [Wed Dec 07 16:02:22 2022 CST]
  10. [8] [01319] [tty1] [        ] [tty1        ] [                    ] [0.0.0.0        ] [Wed Dec 07 16:22:05 2022 CST]
  11. [8] [01320] [tyS0] [        ] [ttyS0       ] [                    ] [0.0.0.0        ] [Wed Dec 07 16:22:05 2022 CST]
  12. [8] [01749] [    ] [        ] [pts/0       ] [                    ] [0.0.0.0        ] [Wed Dec 07 16:22:05 2022 CST]
  13. [1] [00000] [~~  ] [shutdown] [~           ] [3.10.0-1160.80.1.el7.x86_64] [0.0.0.0        ] [Wed Dec 07 16:22:11 2022 CST]
  14. ...
复制代码
各个列与 utmp 文件一致。last 命令读取 wtmp 内容并展示给用户:
  1. > last
  2. yunhai01 pts/0        172.31.22.20     Sat Jan  7 18:15   still logged in   
  3. yunhai01 pts/0        172.31.43.62     Sun Jan  1 11:47 - 11:48  (00:00)   
  4. yunhai01 pts/3        172.31.43.61     Wed Dec 21 20:54 - 14:13  (17:18)   
  5. yunhai01 pts/2        172.31.22.20     Wed Dec 21 14:21 - 18:49  (04:27)   
  6. yunhai01 pts/0        172.31.23.41     Tue Dec 13 11:52 - 11:26 (12+23:34)  
  7. ...
  8. reboot   system boot  3.10.0-1160.80.1 Wed Dec  7 16:22 - 18:44 (31+02:22)  
  9. yunhai01 pts/0        172.31.43.62     Wed Dec  7 16:02 - 16:22  (00:19)   
  10. root     tty1                          Wed Dec  7 15:59 - 16:22  (00:22)   
  11. reboot   system boot  3.10.0-1160.80.1 Wed Dec  7 15:58 - 16:22  (00:23)   
  12. yunhai01 pts/0        172.31.43.62     Wed Dec  7 15:28 - 15:53  (00:25)     
  13. yunhai01 tty1                          Wed Dec  7 15:18 - 15:18  (00:00)   
  14. yunhai01 pts/0        172.31.43.62     Wed Dec  7 14:49 - 15:18  (00:29)   
  15. reboot   system boot  3.10.0-1160.80.1 Wed Dec  7 14:48 - 15:22  (00:33)   
  16. yunhai01 pts/0        172.31.43.62     Wed Dec  7 14:45 - 14:48  (00:02)   
  17. reboot   system boot  3.10.0-1160.80.1 Wed Dec  7 14:45 - 14:48  (00:03)   
  18. yunhai01 pts/0        172.31.43.62     Wed Dec  7 14:44 - 14:45  (00:00)   
  19. reboot   system boot  3.10.0-1160.80.1 Wed Dec  7 14:43 - 14:48  (00:04)   
  20. ...
  21. yunhai01 pts/0        172.31.22.20     Fri Nov 18 11:39 - 14:26  (02:46)   
  22. root     pts/0        172.31.22.20     Fri Nov 18 11:12 - 11:15  (00:02)   
  23. reboot   system boot  3.10.0-1160.76.1 Fri Nov 18 11:00 - 14:48 (19+03:48)  
  24. wtmp begins Fri Nov 18 11:00:28 2022
复制代码
last 读取 wtmp 文件并整理其中的记录,将同一用户的登录登出记为一条记录,分别打印用户名、终端、主机、登录登出时间,通过选项可以控制打印的内容,也可以筛选特定用户、终端的记录,例如只看 pts/2 的记录:
  1. > last pts/2
  2. yunhai01 pts/2        172.31.22.20     Wed Dec 21 14:21 - 18:49  (04:27)   
  3. yunhai01 pts/2        172.31.22.20     Tue Dec 20 15:34 - 17:39  (02:04)   
  4. yunhai01 pts/2        172.31.23.41     Tue Dec 20 12:15 - 13:52  (01:36)   
  5. yunhai01 pts/2        172.31.22.20     Mon Dec 19 16:56 - 07:22  (14:25)   
  6. yunhai01 pts/2        172.31.22.20     Mon Dec 19 10:28 - 15:47  (05:19)   
  7. yunhai01 pts/2        172.31.22.20     Fri Dec  2 14:31 - 16:42  (02:10)   
  8. wtmp begins Fri Nov 18 11:00:28 2022
复制代码
也可以只看用户名:
  1. > last root
  2. root     tty1                          Wed Dec  7 15:59 - 16:22  (00:22)   
  3. root     pts/0        172.31.22.20     Fri Nov 18 11:17 - 11:39  (00:21)   
  4. root     pts/0        172.31.22.20     Fri Nov 18 11:12 - 11:15  (00:02)   
  5. wtmp begins Fri Nov 18 11:00:28 2022
复制代码
默认的展示顺序是新的记录在上面、旧的记录在下面。
btmp

wtmp 记录是登录成功的用户,对于失败的因为没有走到 login 这一步,所以并不会记录下来,btmp 文件是专门用来记录登录失败信息的,一般位于  /var/log/btmp,其中记录的也是 utmp 结构体,可以通过 utmpdump 查看:
  1. > sudo utmpdump /var/log/btmp
  2. Utmp dump of /var/log/btmp
  3. [6] [32150] [    ] [yunhai01] [ssh:notty   ] [172.31.43.61        ] [172.31.43.61   ] [Fri Jan 06 10:57:52 2023 CST]
  4. [6] [32344] [    ] [yunhai01] [ssh:notty   ] [172.31.22.20        ] [172.31.22.20   ] [Sat Jan 07 18:15:18 2023 CST]
复制代码
各个列与 wtmp 无异,只有终端名因未分配而留空。lastb 命令负责读取 btmp 文件:
  1. > sudo lastb
  2. [sudo] password for yunhai01:
  3. yunhai01 ssh:notty    172.31.22.20     Sat Jan  7 18:15 - 18:15  (00:00)   
  4. yunhai01 ssh:notty    172.31.43.61     Fri Jan  6 10:57 - 10:57  (00:00)   
  5. btmp begins Fri Jan  6 10:57:52 2023
复制代码
当怀疑有人尝试破解密码,可以通过 lastb 来定位攻击来源。
最后补充一点,btmp 并不属于 POSIX 标准的一部分,在 mac 上就没有 lastb 命令。
结语

本文介绍了 unix 系统数据文件相关的内容,其中介绍的很多接口都是不可重入的,因此只能在单线程非信号处理器中使用,其实现代 unix 都提供了可重入版本,在现有接口上增加 _r 后缀即可,例如这样就可以在更多的场景中使用它们了。感兴趣的可以查看 man 手册页。
参考

[1]. mac vscode c/c++ 解决include路径问题
[2]. linux用户实现root用户空密码登入
[3]. 【Ubuntu 20.04】useradd 创建用户无法登录图形界面解决方案
[4]. Linux多个文件按列合并的多种场景操作方式
[5]. mac下的strace命令
[6]. Linux笔记:使用stat函数实现ls -l的功能(getpwuid函数 getgrgid函数使用)
[7]. linux /etc/shadow文件详解
[8]. linux用户认证机制
[9]. 模拟密码登陆过程
[10]. ssh免密码登录
[11]. ssh配置指定密钥文件登录linux
[12]. SSH 免密登录(设置后仍需输入密码的原因及解决方法)
[13]. linux用户剔除辅助组,用usermod、gpasswd、Shell Script、Manual Method将用户添加到组
[14]. 使用 utmpdump 监控 CentOS 用户登录历史

来源:https://www.cnblogs.com/goodcitizen/p/things_about_unix_system_data_file_and_information.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具