0%

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

ls

编写ls命令

  • 列出目录的内容
  • 读取并显示文件的属性
  • 给出名字,判断它是目录还是文件

目录

  • 目录是一种特殊的文件,它的内容一条条记录,每一个记录代表一个文件或者目录
  • 目录文件永远不空,至少包含两个特殊项”.”或者”..”

操作目录的相关函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <sys/types.h>
#include <dirent.h>

DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);

// 目录属性
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported
by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};
  • opendir返回值:成功返回一个目录流,失败返回NULL,同时errno设置为相应值

  • readdir返回值:成功返回对应结构体的指针(该结构体被静态分配,不要使用free()释放);发生错误时返回NULL,同时errno会被相应设置;如果到达目录流的末尾,也会返回NULL(errno提前设置为0,不会发生改变)

  • closedir返回值:0表示成功,-1表示发生错误

  • d_type参数:

1
2
3
4
5
6
7
8
DT_BLK      This is a block device.
DT_CHR This is a character device.
DT_DIR This is a directory.
DT_FIFO This is a named pipe (FIFO).
DT_LNK This is a symbolic link.
DT_REG This is a regular file.
DT_SOCK This is a UNIX domain socket.
DT_UNKNOWN The file type could not be determined.

编写ls -l

编写该命令,既需要读取目录文件,也需要根据目录文件中的名称来获取对应的文件信息

ls -l显示示例:

1
2
-rw-rw-r-- 1 zhaoyan zhaoyan 1497 11月 12 14:08 a.txt
drwxrwxr-x 3 zhaoyan zhaoyan 4096 10月 9 18:39 C
  • 模式(mode):首位表示文件类型
    • -:普通文件
    • d:目录
    • c:字符设备文件
    • b:块设备文件
    • l:符号链接文件(Symbolic Links)
    • s:套接字文件
    • p:管道文件
  • 链接数(links):指该文件被引用的次数
  • 文件所有者(owner):
  • 组(group):文件所有者所在的组
  • 大小(size):显示文件的大小,单位是字节,目录所占空间是以块(每块512字节)为单位进行分配的,所以目录的大小经常是相等的
  • 最后修改时间(last-modified)
  • 文件名(name)

stat()函数

进程定义一个struct stat,然后调用stat函数,让内核将文件属性放入这个数据结构中(之前目录结构体dirent是直接返回指针,为什么有这样的区别?)

该函数返回关于一个文件的信息

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
#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) //普通文件


#define S_IRUSR __S_IREAD /* Read by owner. */
#define S_IWUSR __S_IWRITE /* Write by owner. */
#define S_IXUSR __S_IEXEC /* Execute by owner. */
#define S_IRGRP (S_IRUSR >> 3) /* Read by group. */
#define S_IWGRP (S_IWUSR >> 3) /* Write by group. */
#define S_IXGRP (S_IXUSR >> 3) /* Execute by group. */
#define S_IROTH (S_IRGRP >> 3) /* Read by others. */
#define S_IWOTH (S_IWGRP >> 3) /* Write by others. */
#define S_IXOTH (S_IXGRP >> 3) /* Execute by others. */
  • 返回值:如果成功返回0,否则返回-1,并且error会设置为相应的错误类型

如何显示st_mode

  • st_mode是一个16位的二进制数,文件类型和权限被编码在这个数中

    • 高四位:文件类型,最多可以标识16种
    • 中间3位:文件的特殊属性,1表示有,0表示没有。这三位分别是set-user-ID,set-group-IDsticky
    • 最后9位是许可权限,分别对应3种用户(文件所有者,同组用户,其他用户)
  • 所以,只需要通过stat.h文件提供的掩码即可获得

根据用户/组ID寻找name

/etc/passwd文件
1
zhaoyan:x:1000:1000:zhaoyan,,,:/home/zhaoyan:/bin/bash
  • 该文件中每一行代表一个用户,冒号分割不同的字段。
  • 每一个字段依次为:用户名、密码、用户ID、用户所属组ID、用户的全名、主目录、用户使用shell程序的路径
  • 该文件并没有包括所有的用户。在分布式系统中,通常在一台所有人都能访问的主机上保存所有的用户信息,该主机称为NIS,所有的主机通过NIS进行用户身份验证。所有需要用户信息的程序也从NIS上获取。本地只保存所有用户的一个子集以备离线操作。
通过getpwuid得到完整的用户列表

通过库函数getpwuid来访问用户信息(从/etc/passwd或者NIS中获取)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwuid(uid_t uid);

struct passwd {
char *pw_name; /* username */
char *pw_passwd; /* user password */
uid_t pw_uid; /* user ID */
gid_t pw_gid; /* group ID */
char *pw_gecos; /* user information */
char *pw_dir; /* home directory */
char *pw_shell; /* shell program */
};
  • 返回值:成功返回结构体指针,发生错误或者查询为空返回NULL。调用之前请将errno置为0
/etc/group文件
1
zhaoyan:x:1000:
  • 每一个字段依次为组名,组密码,组ID,组中的成员列表

  • passwd文件中每个用户所属的组,是用户的主组(primary group)。用户还可以是其他组的成员,只需要把它的用户名添加到/etc/group中这个组所在行的最后一个字段即可。

通过getgrgid访问组列表
1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/types.h>
#include <grp.h>

struct group *getgrgid(gid_t gid);

struct group {
char *gr_name; /* group name */
char *gr_passwd; /* group password */
gid_t gr_gid; /* group ID */
char **gr_mem; /* NULL-terminated array of pointers
to names of group members */
};
  • 返回值:成功返回结构体指针,发生错误或者查询为空返回NULL。调用之前请将errno置为0

详解st_mode中三个特殊位

set-user-ID位

普通用户可以通过passwd来修改密码,用户密码存放在/etc/passwd中,但是可以看到普通用户并没有权限来修改该文件。
所以passwd命令有一个特殊权限s,使得其特殊属性包含set-user-ID位。SUID位告诉内核,运行这个程序时认为时由该文件所有者在运行这个程序,在这里是root,而root有权限修改/etc/passwd文件。

1
2
-rw-r--r-- 1 root root 2862 10月  9 17:55 /etc/passwd
-rwsr-xr-x 1 root root 59976 3月 14 2022 /usr/bin/passwd

对于一些所有用户共享的文件,普通用户只能以受限方式进行修改,那么就将该文件的所有者设置为root,同时受限命令的所有者添加’s’权限

set-group-ID位

类似于SUID,如果SGID置位,那么程序运行时认为是由该文件所属组中的某一个用户运行

sticky位

  • 对于文件,使用交换(swap)技术会用到该位。现在已经被虚拟内存技术所取代
  • 对于目录,例如用来存放临时文件的/tmp,每一个用户都可以在该目录中创建/删除文件,sticky位使得该目录中的文件只能被创建者删除

特殊属性标识

许可权限位上所有者的s表示set-user-ID,组用户的s表示set-group-ID;其他组的t表示sticky被设置


设置和修改文件的属性

文件类型

  • 文件类型有普通文件、目录文件、设备文件、socket文件、符号链接文件、命名管道(named pipe)文件等。
  • 文件类型是在创建文件时设置的,不同的系统调用创建不同类型的文件(如creat创建一个普通文件)
  • 文件已经创建,其类型就无法修改了

许可位和特殊属性位

每个文件都有9位许可权限和3位特殊属性。它们可以被chmod系统调用修改

建立文件的模式

1
fd = creat("newfile" , 0744);

上述参数只是请求。程序请求的许可权限还会与上”新建文件掩码”(file-creation-mask)来得到文件的最终模式。新建文件掩码umask是一个非常有用的系统变量。例如:

1
2
3
4
5
6
open("a.txt" , O_RDWR | O_CREAT, 0777);

-rwxrwxr-x 1 zhaoyan zhaoyan 0 11月 14 20:02 a.txt

>umask -S
u=rwx,g=rwx,o=rx

可以看到open函数所要求的文件权限0777并没有得到满足,正好是和umask相与之后的结果。

改变文件的模式

使用系统调用chmod,该命令不会收到umask影响,文件许可和特殊属性都可以通过其进行修改。

1
2
3
4
5
6
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);

//mode:建议使用<sys/stat.h>中定义的符号表示,程序可移植性高
chmod("a.txt" , 04764); // 100 111 110 100
chmod("a.txt" , S_ISUID|S_IRWXU|S_IRGRP|S_IWGRP|S_IROTH);
  • 返回值:成功则返回0,发生错误返回-1

文件的链接数

链接数就是文件被引用的次数(别名的数量)。
增加文件的别名(使用link)使链接数增加,减少别名(unlink)使得链接数减少。

文件所有者与组

  • 文件所有者就是创建该文件的用户。如果通过程序创建一个文件,且程序具有SUID位,那么新文件的所有者就是程序文件的所有者

  • 通常,新文件组和创建者的组一致。有些情况下,组会被设置为和父目录的组相同。

修改文件所有者和组

1
2
#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
  • 返回值:成功则返回0,失败则返回-1

或者通过Shell命令chownchgrp也可以修改文件所有者和组

文件大小

不存在能够直接减小文件占用空间的函数

时间

每一个文件都有3个时间:最后修改(modification)时间、最后访问(access)时间和属性最后修改时间。

修改最后修改和访问时间

1
2
3
4
5
6
7
8
#include <sys/types.h>
#include <utime.h>
int utime(const char *filename, const struct utimbuf *times);

struct utimbuf {
time_t actime; /* access time */
time_t modtime; /* modification time */
};
  • 返回值:成功返回0,发生错误返回-1
  • 使用Shell命令touch也可以修改这两个时间

文件名

使用系统调用rename可以修改文件/目录的名字,还可以移动文件的位置。

1
2
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
  • 返回值:成功返回0,遇到错误返回-1

编程练习

ls的完善1

1.为ls02添加支持./ls02 /tmp的功能
- 原始不能支持该功能,因为在使用stat()寻找文件信息时,默认在当前目录下寻找,所以找不到指定目录下的文件信息
- 修改:对于指定目录下的文件,拼接出绝对路径就好了
- strcpy(s1,s2):字符串拷贝,直接将s2拷贝到s1串的起始位置
- strcat(s1,s2):字符串拼接,先寻找s1串的’\0’字符,然后从该位置开始拼接

ls的完善2

在文件ls01.c中实现了ls对-a -l的支持,同时支持排序,分栏显示,颜色显示

分栏显示

系统的ls支持:
1.按序显示
2.按照排序规则按列从上往下显示,当前列显示完成后转到下一列继续显示
3.每一列都是左对齐的
4.每一列的宽度都是该列最长文件名长度加2
5.列的数目要尽量保证每一行都被文件名“填充满”,而又不会导致行中最后一个文件名换行

实现

1.查询屏幕宽度,使用ioctl系统调用

1
2
3
struct winsize size;
ioctl(STDIN_FILENO,TIOCGWINSZ,&size); //获取设备参数
int screen_len = size.ws_col;

2.统计所有filename的总长度,然后除以屏幕宽度,从而得到一个基础行数
3.根据行数计算列数,然后求每一列的宽度(该列中最长的filename,用col_len表示),以及这个最长的filename在第几行(用col_idx表示)。如果所有列总长小于屏幕宽,结束输出行数。否则转4
4.想要减小列总长,只要两列的最大filename能够移动到同一列,则总列长大概率能够缩小(除非一列中最长的filename有多个),所以按照这个思路,只需要第一次满足i*add_row_num >= col_idx即可求得增加的行数,然后转3.

排序

自己实现了快排,具体实现中指针的使用还需要学习

颜色显示

1
2
3
man dircolors
dircolors -p > color
vim color
  • 这样在color文件中即可找到每种类型的文件的默认颜色值
  • printf打印颜色

教训(找了2小时的BUG)

C语言指针、数组、二维、内存分配其乐无穷

1
2
3
4
5
6
7
8
9
10
11
12
13
//改进之前
char *filenames[4096];
filenames[file_cnt++] = direntp->d_name;

struct dirent *readdir(DIR *dirp);

//改进之后
char *filenames[4096];
char *filename = (char *)malloc(sizeof(char)*256);
strcpy(filename , direntp->d_name);
filenames[file_cnt++] = filename;
for(int i = 0; i < file_cnt; i++)
free(filenames[i]);
  • !!问题:在调用getpwuid,getgrgid等函数之后,发现filename[i]中的值神奇的改变了,明明这些系统调用不需要传入filename的。在改进之后问题就消失了。
  • 猜测:改进之前,name的内存是系统调用分配的,然后在使用其他系统调用,这些内存区被改变了?
  • 启示:
    1.如果需要持久化系统调用给的内存,最好自己动态内存分配,在copy过来。
    2.学习这种分配字符串数组的方式

参考