0%

Unix-Linux编程实践教程-章节2笔记

who

  • 把程序的可执行文件放到任意的一个目录中(/bin /usr/bin /usr/local/bin),就可以为系统添加新的命令

寻找who的踪迹

  • man who:发现文件utmp,wtmp很关键
  • man -k utmp:按关键字搜索utmp,发现:
1
2
3
utmp (5)            - login records
login (3) - write utmp and wtmp entries
logout (3) - write utmp and wtmp entries
  • man 5 utmp:发现<utmp.h>中关于utmp结构体的定义

  • 通过阅读以上信息,知道who通过读文件获得需要的信息,而每一个登录用户的信息在文件中都有记录,所以编写who命令分为两步:

    • a.从UTMP_FILE文件中读取数据结构
    • b.将结构体中的信息以适当的形式显示出来

如何读取数据结构

  • man -k file | grep read:将man的输出重定向到grep的输入,grep查找符合条件的字符串,发现:

    • read (2) - read from a file descriptor
    • 注:grep的查找关键字是随意输入的主题相关的单词
  • man 2 read:发现文件操作必须的三个函数read、open、close

1
2
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
  • 返回值:成功则返回读取数量,遇到错误返回-1
1
2
#include <fcntl.h>
int open(const char *pathname, int flags);
  • 参数:
    • flags:O_RDONLY, O_WRONLY, or O_RDWR,分别对应只读,只写,读写三种
  • 返回值:成功则返回文件描述符,遇到错误返回-1
  • 打开文件可能出错的原因多种多样,具体请查看文档
1
2
#include <unistd.h>
int close(int fd);
  • 返回值:0表示成功关闭,-1表示出现错误

通过以上三个函数即可读取登录信息记录文件UTMP_FILE:

1
2
3
4
5
6
7
if((utmpfd = open(UTMP_FILE , O_RDONLY)) == -1)
perror(UTMP_FILE); //用法

while (read(utmpfd , &current_record , len) == len)
show_info(&current_record);

close(utmpfd);

将结构体中的信息显示

主要是以合适格式显示登录时间,通过以下命令查询相关函数:

1
2
man -k time | grep transform
man 3 ctime

函数ctime的信息如下:

1
2
#include <time.h>
char *ctime(const time_t *timep);
  • 返回值:格式为"Wed Jun 30 21:49:08 1993\n"的字符串
  • time_tlong int类型
  • utmp文件中的ut->time表示用户从登录时间到初始时间所经过的秒数

who的工作流程

1.打开UTMP_FILE路径对应的文件
2.读出一个utmp结构,并显示
3.关闭utmp文件

在用户空间中使用缓冲区

1.一次读取16个utmp结构,以供显示
2.当缓冲区中的内容显示完成之后,在进行系统调用,从内核中读取utmp结构数据

内核中的缓冲技术

  • read()将数据从内核缓冲区读取到进程缓冲区。如果进程所要求的数据块不在内核缓冲区,则内核会把对该数据块的请求加入到请求数据队列中,然后将进程挂起;当内核把数据块从磁盘读取到内核缓冲区之后,再把数据从内核缓冲区复制到进程缓冲区,最后再唤醒挂起的进程。
  • write()将数据从进程缓冲区复制到内核缓冲区。内核会把需要写的数据暂时存在缓冲区中,积累到一定数量之后再一次性写入到磁盘中。如果发生意外情况(停电),有可能内核来不及将数据写入到磁盘中,就会导致数据丢失。

修改用户登录信息

1.打开utmp文件
2.查找需要修改的登录记录,并修改
3.关闭文件

文件读取过程

  • 从文件读数据时,从指针标明的地方开始读取指定字节并将指针移动至下一未被读取的字节
  • 指针与文件描述符相关,所以两个程序同时读一个文件时互不影响

lseek()函数

1
2
3
#include <sys/type.h>
#include <unistd.h>
off_t oldpos = lseek(int fd , off_t dist , int base)
  • 参数:
    • dist:移动的距离
    • base:基准位置 SEEK_SET -> 文件开始SEEK_CUR -> 当前位置SEEK_END -> 文件结尾
  • 返回值:返回指针变化前的位置,返回-1表示遇到错误

修改UTMP文件中记录的ut->type

  • ut->type表示该记录的某种状态,例如USER_PROCESS表示正常的登录进程,DEAD_PROCESS表示终止的登录进程
    • 不过用户logout之后并不会修改其为DEAD_PROCESS,不确定什么情况下会使用DEAD_PROCESS来标记登录记录
  • /var/lib/wtmp:记录了本系统中历史登录信息
  • /var/run/utmp:记录当前的登录信息

编写logout_tty()函数

1.打开wtmp文件
2.循环读取,寻找匹配记录
3.定位文件指针到该记录的开始位置
4.写入需要修改的内容
5.关闭文件

  • 通过对比传入的ut_line来修改对应记录的ut->type属性
  • 编写时注意open()传入的flags应为O_RDWR
    • 如果传了O_RDONLY则写入时会perror报/var/log/wtmp: Bad file descriptor
    • Bad file descriptor:如果文件描述符有问题,也会报出这个错误(一般文件描述符不会太大,太大时八成是没有初始化)
  • 查看一下/var/lib/wtmp的信息以及当前用户和root用户的所属组:
    • 该文件属于root用户,而我们和root用户不同组,属于其他组,没有写入权限,报/var/log/wtmp: Permission denied
    • 可以切换成root用户再执行
1
2
3
4
5
6
//wtmp信息
-rw-rw-r-- 1 root utmp 22656 10月 26 16:54 /var/log/wtmp

// 用户所属组信息
zhaoyan : zhaoyan adm cdrom sudo dip plugdev lpadmin lxd sambashare
root : root

cp

  • 拷贝:cp source_file target_file

使用的系统调用

1
2
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
  • 参数:
    • mode:访问模式(mode_t应该是unsigned int类型)
  • 返回值:成功返回文件描述符,失败返回-1
  • 没有则创建文件,如果文件存在则它的内容会被清空。
1
2
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
  • 返回值:成功则返回写入的字节数,失败则返回-1
  • 为什么写入字节数会少于指定数量:
    • 系统对文件的最大长度有限制,或者磁盘空间不足
    • 因此有必要校验是否完全写入

linux权限

例如-rw--r--r-:

  • 第一位表示文件类型,-表示文件,d表示目录
  • 从左到右分别是文件所有者的权限(u),文件所有者所属组成员的权限(g),所有者所属组之外的用户的权限(o)
  • r对应4,w对应2,x对应1,-对应0,所以可以用0644来描述(0表示该数是一个八进制数
  • 权限一般用八进制表示,因为二进制每三位对应一组权限,用十六或者十进制都得需要计算一下,不如八进制方便

错误提示

  • 在<errno.h>中包含错误类型和相应的说明。当系统调用出错时,就会把该文件中的errno全局变量设置为相应的错误代码,然后返回-1。程序可以通过检查errno来确定错误类型,并采取相应的措施。
  • 更方便是使用perror()函数,它可以自己查找错误代码,并在标准错误输出中显示相应的错误信息,string是同时显示的描述信息
1
2
#include <stdio.h>
void perror(const char *s);

cat

1.判断pathname是一个文件还是目录
2.打开该文件,并通过缓存读取并输出
3.关闭该文件

如何判断是文件还是目录

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
- man 2 stat

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *statbuf);


struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */ 索引编号
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */

/* Since Linux 2.6, the kernel supports nanosecond
precision for the following timestamp fields.
For the details before Linux 2.6, see NOTES. */

struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */

#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};

//检查文件类型
#define __S_ISTYPE(mode, mask) (((mode) & __S_IFMT) == (mask))

#define S_ISDIR(mode) __S_ISTYPE((mode), __S_IFDIR) //目录
#define S_ISCHR(mode) __S_ISTYPE((mode), __S_IFCHR) //字符特殊设备
#define S_ISBLK(mode) __S_ISTYPE((mode), __S_IFBLK) //块特殊设备
#define S_ISREG(mode) __S_ISTYPE((mode), __S_IFREG) //普通文件
  • 返回值:如果成功返回0,否则返回-1,并且error会设置为相应的错误类型

习题

习题写了cat head last命令,
其中last命令,由于没有找到用户logout信息在哪里存放,所以无法和系统的last保持一致
此外,与head命令相对应的tail命令不是很好写,难在“怎么定位一个文件的倒数10行开始处”这个问题怎么优雅的解决