文件系统编程概述
三种类型的函数对比
ISO C 标准库函数:
- 这些函数是由 ISO(国际标准化组织)提供的,具有最好的通用性和跨平台性。在所有遵循 ISO 标准的现代 C 编译平台上,这些函数的行为都是一致的。
- 这些函数提供了一些通用的功能实现,例如:
printf
、scanf
、fopen
、malloc
等。 -
它们在 man 手册的第 3 部分(3 号手册)中有所描述。
POSIX 标准库函数:
- POSIX-C 标准旨在为不同的类 Unix 操作系统提供一致的编程接口,以实现应用程序的跨平台移植。
- 这些函数同样是库函数,也在 man 手册的第 3 部分中描述。
-
典型的 POSIX 标准库函数,如
opendir
和closedir
,用于在 Linux 环境下操作目录文件。
POSIX 标准系统调用函数:
- 这些是遵循 POSIX 标准的系统调用接口,它们在所有类 Unix 平台上较为通用。
-
系统调用函数是 Linux 系统内核和应用程序交互的接口。它们直接与内核交互,因此其通用性和跨平台性相对较差,因为它们的实现依赖于内核的架构和细节。
函数的返回值和形参
在 C 语言中,函数返回值常用于指示函数是否成功执行。如果函数返回一个特殊的值,这通常表示发生了错误。
返回值类型与错误标志:
- 函数返回值类型为
void
时,表示函数不返回任何值,通常不需要进行错误处理。 - 当函数返回
int
类型时,错误标志通常是-1
。 - 对于返回
size_t
类型的函数,错误标志通常是0
,因为size_t
是一个无符号整数类型。 - 返回
ssize_t
类型的函数,错误标志同样是-1
,ssize_t
是有符号整数类型。 - 如果函数返回指针类型,错误标志通常是
NULL
,表示未找到或无法分配内存。
指针返回值:
- 指针类型的返回值表示函数可能在堆上分配了内存或获取了资源。这通常意味着需要手动管理这些资源,如释放内存或关闭资源,除非文档中明确指出不需要这样做。
在查看函数的参数列表时,指针类型的参数尤其重要。需要检查指针是否被 const
修饰:
- 被
const
修饰的指针参数表示函数不会修改它指向的内存区域。 - 未被
const
修饰的指针参数表示函数可能会修改它指向的内存区域。
目录流相关函数
chmod
改变文件权限
chmod
是一个在 Unix 和类 Unix 系统中广泛使用的系统调用,用于改变文件或目录的权限。这个函数定义在 POSIX 标准中,并且通常在 C 语言的 sys/stat.h
头文件中声明。
函数原型 int chmod(const char *pathname, mode_t mode);
包含两个参数:
pathname
:一个指向以 null 结尾的字符串的指针,这个字符串包含了要改变权限的文件或目录的路径。mode
:一个新的文件模式,它定义了文件或目录的权限。这个模式是一个位掩码,通常使用宏来表示不同的权限设置,例如S_IRUSR
(用户读权限)、S_IWUSR
(用户写权限)、S_IXUSR
(用户执行权限)等。
函数的返回值:
- 如果函数成功执行,返回 0。
- 如果函数执行失败,返回 -1,并设置全局变量
errno
以指示错误类型。
mode_t
是一个在 <sys/types.h>
中定义的类型,通常是一个无符号整数类型,用于表示文件的模式。
下面是一个示例:
int main(int argc, char *argv[]){
ARGS_CHECK(argc, 3);
mode_t mode;
sscanf(argv[2], "%o", &mode ); // %o表示八进制无符号数
int ret = chmod(argv[1], mode);
ERROR_CHECK(ret, -1, "chmod");
return 0;
}
需要注意的是:设置权限时需要传参权限的 “数字表示法”,而且设定的权限就是文件的最终权限,掩码只会影响新建文件,不会影响 chmod
函数。
getcwd
获取当前工作目录的完整路径
getcwd
是一个 C 语言标准库函数,用于获取当前工作目录的完整路径。这个函数定义在 POSIX 标准中,并且通常在 C 语言的 unistd.h
头文件中声明。
函数原型 char *getcwd(char *buf, size_t size);
包含两个参数:
buf
:一个指向字符数组的指针,用于存储当前工作目录的路径。如果这个参数是NULL
,函数会分配一个足够大的缓冲区来存储路径,并且返回指向这个缓冲区的指针。注意,如果buf
不是NULL
,调用者需要确保它有足够的空间来存储完整的路径。size
:缓冲区的大小,即buf
可以存储的字符数,包括最后的空字符('\0'
)。
函数的返回值:
- 如果函数成功执行,返回指向
buf
的指针,其中包含了当前工作目录的路径。 - 如果函数执行失败,返回
NULL
,并设置全局变量errno
以指示错误类型。
使用 getcwd
函数时,需要特别注意缓冲区溢出的问题。如果 size
参数指定的大小不足以存储完整的路径,函数将失败,并且 errno
将被设置为 ERANGE
。
下面是一个示例:
int main(void){
// 方式1:直接用栈数组作为getcwd返回值数组
char path[1024] = {0};
char *p = getcwd(path,sizeof(path));
ERROR_CHECK(p,NULL,"getcwd");
printf("cwd = %s\n", path);
// 方式2:动态分配一个堆数组
// char *path = (char *)malloc(1024);
// char *p = getcwd(path,1024);
// free(path);
}
chdir
改变当前目录
chdir
是一个在 Unix 和类 Unix 系统中广泛使用的系统调用,用于改变当前工作目录。这个函数定义在 POSIX 标准中,并且通常在 C 语言的 unistd.h
头文件中声明。
函数原型 int chdir(const char *path);
包含一个参数:path
:一个指向以 null 结尾的字符串的指针,这个字符串包含了要切换到的目录的路径。
函数的返回值:
- 如果函数成功执行,返回 0。
- 如果函数执行失败,返回 -1,并设置全局变量
errno
以指示错误类型。
使用 chdir
函数可以改变程序的当前工作目录,这会影响后续的文件操作,如打开文件、创建文件等。这些操作默认都是在当前工作目录下进行的。
下面是一个示例:
int main(int argc, char *argv[]) {
ARGS_CHECK(argc, 2); // 命令行参数必须2个,第二个是要切换的目标目录
// 先获取当前工作目录,然后打印
char buf[1024] = {0};
char *ret = getcwd(buf, sizeof(buf));
ERROR_CHECK(ret, NULL, "getcwd");
printf("一开始的工作目录是: ");
puts(buf);
// 改变当前工作目录
int ret2 = chdir(argv[1]);
ERROR_CHECK(ret2, -1, "chdir");
// 再次打印当前工作目录
char *ret3 = getcwd(buf, sizeof(buf));
ERROR_CHECK(ret3, NULL, "getcwd");
printf("chdir后的工作目录是: ");
puts(buf);
return 0;
}
每个进程都有自己的当前工作目录,这是进程解析相对路径的起点。
当父进程创建子进程时,子进程会继承父进程的当前工作目录。这意味着子进程在创建时,其工作目录与父进程相同。
Shell 与子进程:
- Shell(如 bash)是一个交互式命令行界面,它允许用户执行命令。当在 Shell 中执行一个可执行程序时,会创建一个新的子进程。
- 子进程在创建时继承了 Shell 的当前工作目录,但子进程可以独立地改变其工作目录,这不会影响父进程(Shell)的工作目录。
chdir
函数与 cd
命令:
chdir
函数是一个系统调用,用于改变调用进程的当前工作目录。它与 Shell 中的cd
命令不同,因为cd
命令实际上会改变 Shell 进程的当前工作目录,而不仅仅是当前执行的程序。- 子进程中使用
chdir
函数修改工作目录的操作是局部的,只影响该子进程,而不影响父进程。
cd
指令的原理: cd
并不是一个独立的外部程序,而是 Shell(如 bash)的内置命令。这意味着 cd
作为 Shell 的一部分,直接修改当前 Shell 进程的工作目录。
为什么 which cd
找不到可执行程序: 由于 cd
是 Shell 的内置命令,它不存在于文件系统中作为独立的可执行文件。因此,使用 which
命令查找 cd
会失败,因为 which
仅用于查找外部可执行文件的路径。
cd
指令的作用:cd
命令允许用户更改当前 Shell 会话的工作目录。这对于导航到不同目录以执行命令或访问文件非常有用。
mkdir
创建目录
mkdir
是一个在 Unix 和类 Unix 系统中广泛使用的系统调用,用于创建一个新的目录。这个函数定义在 POSIX 标准中,并且在 C 语言的 sys/stat.h
头文件中声明。
函数原型 int mkdir(const char *pathname, mode_t mode);
包含两个参数:
pathname
:一个指向以 null 结尾的字符串的指针,这个字符串包含了要创建的目录的路径。mode
:新目录的权限模式。这个模式是一个位掩码,定义了目录的权限。这个参数与chmod
函数中使用的模式相同,通常使用宏来表示不同的权限设置,例如S_IRWXU
(所有者读、写、执行权限)、S_IRWXG
(组读、写、执行权限)等。
函数的返回值:
- 如果函数成功执行,返回 0。
- 如果函数执行失败,返回 -1,并设置全局变量
errno
以指示错误类型。
mode_t
是一个在 <sys/types.h>
中定义的类型,通常是一个无符号整数类型,用于表示文件的模式。
int main(int argc, char* argv[]) {
ARGS_CHECK(argc, 3); // 需要三个命令行参数,允许传入一个三位八进制数表示权限
mode_t mode;
sscanf(argv[2], "%o", &mode); // 将第三个命令行参数字符串转换成八进制无符号整数
int ret = mkdir(argv[1], mode);
ERROR_CHECK(ret, -1, "mkdir");
return 0;
}
umask
的作用: umask
是一个系统设置,它定义了新创建的文件和目录的默认权限。umask
的值通常是一个三位的八进制数,每一位分别对应文件的拥有者、组用户和其他用户的权限。
umask
的影响:
- 当创建新目录时,如果设置的权限为 777,而
umask
设置为 002,那么最终目录的权限将是 775(即 777 – 002)。 - 这种计算方式同样适用于文件权限的设置。
查看和设置 umask
:
- 使用
umask
命令可以查看当前的文件掩码值。 - 使用
umask n
命令可以设置一个新的文件掩码值,其中n
是一个八进制数。
umask
的重要性:
umask
的存在是为了提高系统的安全性,防止新创建的文件和目录具有过于宽松的权限设置。- 较低的
umask
值会导致更开放的权限设置,而较高的umask
值会导致更严格的权限设置。
rmdir
删除空目录
rmdir
是一个 POSIX 标准定义的系统调用,用于删除一个空目录。这个函数通常在 C 语言的 unistd.h
头文件中声明。
函数原型 int rmdir(const char *pathname);
包含一个参数:
pathname
:一个指向以 null 结尾的字符串的指针,这个字符串指定了要删除的目录的路径。
函数的返回值:
- 如果函数成功执行,返回 0。
- 如果函数执行失败,返回 -1,并设置全局变量
errno
以指示错误类型。
使用 rmdir
函数时,需要注意以下几点:
- 只有当目录是空的,即不包含任何文件和子目录时,
rmdir
才能成功删除该目录。 - 如果目录不是空的,
rmdir
调用将失败,并且errno
将被设置为ENOTEMPTY
。 - 如果指定的路径不存在,或者调用者没有足够的权限删除该目录,
rmdir
也会失败。
一个示例如下:
int main(int argc, char* argv[]) {
ARGS_CHECK(argc, 2);
int ret = rmdir(argv[1]); // 注意:rmdir 只能删除空目录
ERROR_CHECK(ret, -1, "rmdir");
return 0;
}
opendir
打开目录流
pendir
是一个 POSIX 标准定义的函数,用于打开一个目录流,以便可以读取其中的内容。这个函数通常在 C 语言的 dirent.h
头文件中声明。
函数原型 DIR *opendir(const char *name);
包含一个参数:name
:一个指向以 null 结尾的字符串的指针,这个字符串指定了要打开的目录的路径。
函数的返回值:
- 如果函数成功执行,返回一个指向新分配的
DIR
结构的指针,该结构表示目录流。 - 如果函数执行失败,返回
NULL
,并设置全局变量errno
以指示错误类型。
使用 opendir
打开目录流后,可以使用 readdir
函数来读取目录中的条目,使用 rewinddir
函数来重置目录流的位置,以及使用 closedir
函数来关闭目录流并释放相关资源。
closedir
关闭目录流
closedir
是一个 POSIX 标准定义的函数,用于关闭之前使用 opendir
函数打开的目录流,并释放与之关联的所有资源。这个函数通常在 C 语言的 dirent.h
头文件中声明。
函数原型 int closedir(DIR *dirp);
包含一个参数 dirp
:一个指向 DIR
结构的指针,该结构是通过 opendir
函数返回的,表示要关闭的目录流。
函数的返回值:
- 如果函数成功执行,返回 0。
- 如果函数执行失败,返回 -1,并设置全局变量
errno
以指示错误类型。
使用 closedir
函数时,通常在完成对目录内容的遍历之后调用,以确保及时释放系统资源。如果 closedir
调用失败,可以通过检查 errno
来确定错误原因,常见的错误可能包括传入的 dirp
指针无效或者内存分配失败等。
readdir
读目录流
readdir
是一个 POSIX 标准定义的函数,用于从之前使用 opendir
打开的目录流中读取下一个目录项。这个函数通常在 C 语言的 dirent.h
头文件中声明。
函数原型 struct dirent *readdir(DIR *dirp);
包含一个参数:
dirp
:一个指向DIR
结构的指针,该结构是通过opendir
函数返回的,表示要读取的目录流。
函数的返回值:
- 如果函数成功执行,返回一个指向
struct dirent
的指针,该结构包含了目录中下一个条目的信息。 - 如果已经到达目录的末尾,返回
NULL
。 - 如果函数执行失败,也返回
NULL
,并设置全局变量errno
以指示错误类型。
struct dirent
结构通常包含以下成员:
d_ino
:一个整数,表示条目的 inode 号(在某些系统上可能不可用)。d_off
:一个长整数,表示目录流中的偏移量,可以用于seekdir
或telldir
函数。d_reclen
:一个短整数,表示struct dirent
结构的长度。d_type
: 类型(type)。这是一个指示目录项类型的标志,例如,是否是文件、目录、符号链接等。这个字段不是 POSIX 标准的一部分,但在某些系统(如 GNU/Linux)中可用。d_name
:一个字符数组,包含了条目的名称。
// dirent 是 directory entry 的简写,就是目录项的意思
struct dirent {
ino_t d_ino; // 此目录项的 inode 编号,目录项中会存储文件的 inode 编号。一般是一个 64 位无符号整数(64 位平台)
off_t d_off; // 到下一个目录项的偏移量。可以视为指向下一个目录项的指针(近似可以看成链表),一般是一个 64 位有符号整数
unsigned short d_reclen; // 此目录项的实际大小长度,以字节为单位(注意不是目录项所表示文件的大小,也不是目录项结构体的大小)
unsigned char d_type; // 目录项所表示文件的类型,用不同的整数来表示不同的文件类型
char d_name[256]; // 目录项所表示文件的名字,该字段一般决定了目录项的实际大小。也就是说文件名越长,目录项就越大
};
其中文件类型 d_type
的可选值如下(使用宏常量定义的整数):
DT_BLK // 块设备文件,对应整数值 6
DT_CHR // 字符设备文件,对应整数值 2
DT_DIR // 目录文件,对应整数值 4
DT_FIFO // 有名管道文件,对应整数值 1
DT_LNK // 符号链接文件,对应整数值 10
DT_REG // 普通文件,对应整数值 8
DT_SOCK // 套接字文件,对应整数值 12
DT_UNKNOWN // 未知类型文件,对应整数值 0
seekdir
和 telldir
telldir
和 seekdir
是两个与目录流操作相关的 POSIX 标准函数,它们允许你在目录流中获取当前位置和设置新位置。
telldir
函数原型:
long telldir(DIR *dirp);
参数:dirp
:指向 DIR
结构的指针,该结构是通过 opendir
函数返回的,表示要查询的目录流。
功能:telldir
函数返回一个值,表示当前目录流的位置。这个位置是一个偏移量,可以用于 seekdir
函数来重新定位目录流。
返回值:
- 返回一个
long
类型的值,表示当前目录流中的偏移量。 - 如果出现错误,返回
(-1L)
并设置errno
以指示错误。
seekdir
函数原型:
void seekdir(DIR *dirp, long loc);
参数:
dirp
:指向DIR
结构的指针,该结构是通过opendir
函数返回的,表示要设置位置的目录流。loc
:要设置的目录流中的偏移量。
功能:seekdir
函数将目录流的位置设置为由 loc
指定的偏移量。这个偏移量应该由先前的 telldir
调用返回。
返回值:
- 此函数不返回任何值(
void
类型)。 - 如果出现错误,会设置
errno
以指示错误。
这两个函数通常一起使用,以实现目录流的定位和重定位。例如,可以先使用 telldir
保存当前位置,然后使用 seekdir
跳转到目录中的特定位置,最后再次使用 telldir
来获取新位置。
int main(int argc, char *argv[]) {
ARGS_CHECK(argc, 2);
DIR *dirp = opendir(argv[1]);
ERROR_CHECK(dirp, NULL, "opendir");
struct dirent *dp; // 指向目录项结构体的指针
// 需求: 记录 dir2 目录项,然后在 while 循环的下面重新打印这个目录项
long location;
while ((dp = readdir(dirp)) != NULL) {
// 每一次循环就处理一个目录项
printf(
"inode num = %lu"
"offset = %ld"
"reclen = %hu"
"type = %u"
"file name = %s\n",
dp->d_ino, dp->d_off, dp->d_reclen, dp->d_type, dp->d_name);
if (strcmp("..", dp->d_name) == 0) {
// 记录 dir2 目录项的位置
location = telldir(dirp);
}
} // 当 while 循环结束时,dp 就是空指针
printf("----------------------------\n");
seekdir(dirp, location);
dp = readdir(dirp);
printf(
"inode num = %lu"
"offset = %ld"
"reclen = %hu"
"type = %u"
"file name = %s\n",
dp->d_ino, dp->d_off, dp->d_reclen, dp->d_type, dp->d_name);
// 不要忘记关闭流
int ret = closedir(dirp);
ERROR_CHECK(ret, -1, "closedir");
return 0;
}
rewinddir
目录流重置
rewinddir
是一个 POSIX 标准定义的函数,用于将目录流的位置重置到开始的位置,即目录的第一个条目。这个函数通常在 C 语言的 dirent.h
头文件中声明。
函数原型:
void rewinddir(DIR *dirp);
参数:dirp
:指向 DIR
结构的指针,该结构是通过 opendir
函数返回的,表示要重置的目录流。
功能:rewinddir
函数将目录流中的当前位置重置到目录的第一个条目。这允许程序重新遍历目录中的所有条目。
返回值:
- 此函数不返回任何值(
void
类型)。 - 如果出现错误,会设置
errno
以指示错误。
使用 rewinddir
可以方便地重新访问目录中的条目,而不需要关闭并重新打开目录流。这在需要多次遍历同一目录的情况下非常有用。
stat
获取元数据
stat
函数是一个广泛使用的 POSIX 标准函数,用于获取文件或目录的元数据(如大小、权限、时间戳等)。这个函数在 C 语言的 sys/stat.h
头文件中声明。
函数原型:
int stat(const char *path, struct stat *buf);
参数:
path
:一个指向以 null 结尾的字符串的指针,指定了要获取信息的文件或目录的路径。buf
:一个指向struct stat
结构的指针,该结构用于接收关于path
指定的文件或目录的信息。
struct stat
结构包含了以下一些常见的成员(具体成员可能因系统而异):
st_dev
:设备编号st_ino
:inode 号(文件系统的唯一标识符)st_mode
:文件类型和权限st_nlink
:硬链接数量st_uid
和st_gid
:文件所有者的用户 ID 和组 IDst_size
:文件大小(字节)st_atime
,st_mtime
,st_ctime
:分别是文件的最后访问时间、最后修改时间和元数据最后改变时间的时间戳
功能:
stat
函数尝试获取path
指定的文件或目录的元数据,并将结果存储在buf
指向的struct stat
结构中。
返回值:
- 如果函数成功执行,返回 0。
- 如果函数执行失败,返回 -1,并设置全局变量
errno
以指示错误类型。
目录流获取的文件名:在使用目录流读取目录项时,我们可以获得每个文件的文件名。但这并不一定是文件的完整路径名。
- 当当前工作目录与要打印目录信息的目录相同(例如,都是
/dir
),此时从目录流获取的文件名可以直接作为stat
函数的参数,因为它们是相对于当前工作目录的相对路径名。例如,如果
/dir
目录下有三个文件:file1
、file2
、file3
,那么它们的相对路径名分别是file1
、file2
、file3
。在这种情况下,文件名可以直接作为stat
函数的参数,因为它们已经是相对于当前工作目录的路径。 -
如果当前工作目录与要打印目录信息的目录不同(例如,当前工作目录是
/dir
,但要打印的是另一个目录/subdir
的信息),则从目录流获取的文件名不是完整的路径名。
- 例如,如果
/subdir
目录下有三个文件:file1
、file2
、file3
,而当前工作目录是/dir
,则这些文件的绝对路径应该是/subdir/file1
、/subdir/file2
、/subdir/file3
。如果使用/dir
作为工作目录,直接使用文件名作为stat
函数的参数将无法正确找到这些文件。
从目录流获取的文件名是否可以直接作为 stat
函数的参数,取决于当前工作目录是否与要查询的目录相同。如果不同,需要将文件名与目录路径拼接成完整的路径名。
int main(int argc, char *argv[]) {
ARGS_CHECK(argc, 2);
DIR *dirp = opendir(argv[1]);
ERROR_CHECK(dirp, NULL, "opendir");
// 解决方法 1:切换工作目录为要打印信息的目录
// int ret = chdir(argv[1]);
// ERROR_CHECK(ret, -1, "chdir");
struct dirent *dp;
while ((dp = readdir(dirp)) != NULL) {
struct stat stat_buf;
// 循环处理目录项,把文件名直接作为路径名传参给 stat 函数
// 这里需要配合解决办法 1
// int ret = stat(path, &stat_buf);
// ERROR_CHECK(ret, -1, "stat");
// 解决办法 2:拼接文件的绝对路径然后再传参给 stat 函数
char path[1024] = {0};
// strcpy(path, argv[1]);
// strcat(path, "/");
// strcat(path, dp->d_name);
sprintf(path, "%s%s%s", argv[1], "/", dp->d_name);
int ret = stat(path, &stat_buf);
ERROR_CHECK(ret, -1, "stat");
// 获取 stat 结构体后打印文件的详细信息
printf("%o %lu %u %u %lu %ld %s\n", stat_buf.st_mode, stat_buf.st_nlink,
stat_buf.st_uid, stat_buf.st_gid, stat_buf.st_size,
// stat_buf.st_mtim.tv_sec,
stat_buf.st_mtime, dp->d_name);
}
closedir(dirp);
return 0;
}
关于 struct stat
结构体:
struct stat
是一个在 POSIX 兼容的操作系统中定义的数据结构,用于存储文件或目录的元数据。当你调用像 stat
或 lstat
这样的函数时,它们会将关于文件的信息填充到一个 struct stat
结构体实例中。这个结构体定义在 sys/stat.h
头文件中。
struct stat
结构体通常包含以下成员(成员可能根据不同的系统和编译环境有所不同):
struct stat {
mode_t st_mode; // 包含文件的类型以及权限信息
nlink_t st_nlink; // 文件的硬链接数量
uid_t st_uid; // 文件所有者的用户ID
gid_t st_gid; // 文件所有者组的组ID
off_t st_size; // 文件的实际大小, 以字节为单位
struct timespec st_mtim; /* 包含文件最后修改时间的结构体对象 */
};
这些字段的类型都是使用别名来定义的,在 64 位 Linux 操作系统上,这些别名的类型一般是:
mode_t
:一般是一个 32 位无符号整数。nlink_t
:一般是一个 64 位无符号整数。uid_t
和uid_t
:一般是一个 32 位无符号整数。off_t
:一般是一个 64 位无符号整数。
其中的 timespec
结构体定义如下:
struct timespec {
time_t tv_sec; // 秒,表示时间戳中的秒部分
long tv_nsec; // 纳秒,表示时间戳中的微秒部分,用于更精确地表示时间
};
tv_sec
:表示时间戳的秒部分,通常与历元(Epoch)的时间差有关。tv_nsec
:表示时间戳的纳秒部分,即一秒内的微秒数,可以提供额外的时间精度。
由于宏 #define st_mtime spec.tv_sec
的存在,可以直接使用 statbuf.st_mtime
来访问 statbuf.st_mtim.tv_sec
的值。
statbuf.st_mtim.tv_sec
和 statbuf.st_mtime
是等价的,都表示文件的最后修改时间的秒部分。
注意:
- 一个是
st_mtim
,这是一个结构体类型,它是stat
结构体的成员。 - 一个是宏定义
st_time
,它是一个long int
类型的表示文件最后修改时间的时间戳秒数。
getpwuid
获取用户信息
getpwuid
是一个在 POSIX 标准中定义的函数,用于根据用户 ID(uid
)获取用户的信息。这个函数通常在 C 语言的 pwd.h
头文件中声明。
函数原型:
struct passwd *getpwuid(uid_t uid);
参数:
uid
:用户 ID,是一个无符号整数,表示要获取信息的用户的唯一标识符。
功能:
getpwuid
函数在系统用户数据库中查找与给定uid
相对应的用户记录,并返回一个指向struct passwd
结构的指针,该结构包含了用户的信息。
返回值:
- 如果找到对应的用户记录,函数返回一个指向
struct passwd
结构的指针。 - 如果没有找到对应的用户记录或发生错误,函数返回
NULL
并设置全局变量errno
以指示错误类型。
struct passwd
结构通常包含以下成员:
pw_name
:一个指针,指向一个字符串,表示用户的登录名。pw_passwd
:一个指针,指向一个字符串,表示用户的密码。出于安全考虑,通常密码以加密形式存储。pw_uid
:用户的用户 ID。pw_gid
:用户的组 ID。pw_gecos
:一个指针,指向描述用户信息的字符串,如全名、工作电话、家庭电话等。pw_dir
:一个指针,指向用户主目录的路径。pw_shell
:一个指针,指向用户默认登录 shell 的路径。
struct passwd {
char *pw_name; // 用户名
char *pw_passwd; // 密码(通常是加密后的密码,但在现代系统中通常是 'x' 或 '*',真正的密码存储在安全文件中)
uid_t pw_uid; // 用户ID
gid_t pw_gid; // 组ID
char *pw_gecos; // GECOS 字段,包含其他用户信息
char *pw_dir; // 用户主目录
char *pw_shell; // 用户登录 shell
};
getgrgid
获取组信息
getgrgid
是一个在 POSIX 标准中定义的函数,用于根据组 ID(gid
)获取组的信息。这个函数通常在 C 语言的 grp.h
头文件中声明。
函数原型:
struct group *getgrgid(gid_t gid);
参数:gid
:组 ID,是一个无符号整数,表示要获取信息的组的唯一标识符。
功能:getgrgid
函数在系统组数据库中查找与给定 gid
相对应的组记录,并返回一个指向 struct group
结构的指针,该结构包含了组的信息。
返回值:
- 如果找到对应的组记录,函数返回一个指向
struct group
结构的指针。 - 如果没有找到对应的组记录或发生错误,函数返回
NULL
并设置全局变量errno
以指示错误类型。
struct group
结构通常包含以下成员:
gr_name
:一个指针,指向一个字符串,表示组的名称。gr_passwd
:一个指针,指向一个字符串,表示组的密码。通常这个字段不使用或包含一个占位符。gr_gid
:组的组 ID。gr_mem
:一个指向字符串数组的指针,数组中包含了属于该组的所有用户的登录名列表。
struct group {
char *gr_name; // 组名
char *gr_passwd; // 组密码(在现代系统中通常是 'x' 或 '*',真正的密码存储在安全文件中)
gid_t gr_gid; // 组ID
char **gr_mem; // 包含组成员用户名的字符串数组
};
localtime
时间函数
localtime
是 C 标准库中的一个函数,用于将自纪元(Epoch,即 1970 年 1 月 1 日 00:00:00 UTC)以来的秒数(time_t
类型)转换为一个更易读的结构化时间表示,即 struct tm
类型。这个函数定义在 time.h
头文件中。
函数原型:
struct tm *localtime(const time_t *timer);
参数:timer
:一个指向 time_t
类型的指针,包含了要转换的时间值,表示自纪元以来的秒数。
功能:localtime
函数将 timer
指向的时间戳转换为当地时间(考虑了时区和夏令时设置)的 struct tm
格式。
返回值:
- 函数返回一个指向
struct tm
结构的指针,该结构包含了转换后的时间信息。 - 如果转换失败,返回
NULL
并设置全局变量errno
以指示错误类型。
struct tm
结构通常包含以下成员:
tm_sec
:秒(0-59)tm_min
:分钟(0-59)tm_hour
:小时(0-23)tm_mday
:一个月中的第几天(1-31)tm_mon
:年份中第几个月,从 0(1 月)开始计数tm_year
:自 1900 年以来的年数(例如,2023 表示为 123)tm_wday
:一周中的第几天,从 0(通常表示星期日)开始计数tm_yday
:一年中的第几天,从 0(1 月 1 日)开始计数tm_isdst
:夏令时标志,非零表示夏令时,零表示标准时间
struct tm {
int tm_sec; /* 秒 – 取值区间为[0,59] */
int tm_min; /* 分 - 取值区间为[0,59] */
int tm_hour; /* 时 - 取值区间为[0,23] */
int tm_mday; /* 一个月中的日期 - 取值区间为[1,31] */
int tm_mon; /* 月份(从一月开始,0代表一月)- 取值区间为[0,11] */
int tm_year; /* 年份,其值从1900开始 */
int tm_wday; /* 星期 – 取值区间为[0,6],星期日为0 */
int tm_yday; /* 从每年的1月1日开始的天数 – 取值区间为[0,365],1月1日为0 */
int tm_isdst; /* 夏令时标识符,夏令时时为正,不是夏令时时为0,不了解情况时为负 */
};
目录流和文件流
目录流(DIR *
)和文件流(如 FILE *
)是文件 I/O 操作中的两种不同类型,它们在操作流程和使用方式上有一些相似之处,但也有明显的区别。
相似点:
- 操作流程:无论是文件流还是目录流,操作流程都是相同的,即先打开流,然后进行操作,最后关闭流。
- 操作位置指示:打开目录文件后,会得到一个目录流指针
DIR *
,类似于文件流的FILE *
指针,它指示了操作的位置。 - 库函数封装:文件流和目录流都是库函数,它们封装了系统调用,提供了相似的使用方式。
不同点:
- 写操作:最明显的不同是目录流不允许写操作,只能进行读操作。系统内核没有提供写目录流的功能。
- 库函数标准:文件流通常遵循 ISO C 标准库函数,而目录流则遵循 POSIX 标准库函数。
文件流 | 目录流 |
---|---|
fopen | opendir |
fclose | closedir |
fread | readdir |
fwrite | × |
ftell | telldir |
fseek | seekdir |
rewind | rewinddir |
实现 ll 指令
简易版
使用 opendir
,closedir
和 readdir
实现一个简易版指令
#include <stdc.h>
int main(int argc, char *argv[]) {
ARGS_CHECK(argc, 2);
DIR *dirp = opendir(argv[1]);
// 打开目录流需要进行错误处理
ERROR_CHECK(dirp, NULL, "opendir");
struct dirent *pdirent;
// 循环读目录项并打印目录信息,循环结束的条件是返回值为 NULL
while ((pdirent = readdir(dirp)) != NULL) {
printf(
"inode num = %lu, reclen = %hu, type = %u, name = %s\n",
pdirent->d_ino, // 64 位平台这个 inode 编号一般是一个 64 位无符号整数
pdirent->d_reclen, // 无符号短整型
pdirent->d_type, // 无符号整型
pdirent->d_name); // 以字符串类型打印
}
// 及时释放资源,关闭目录流
closedir(dirp);
return 0;
}
输出结果如下:
优化
#include <stdc.h>
int main(int argc, char *argv[]) {
ARGS_CHECK(argc, 2);
DIR *dirp = opendir(argv[1]);
ERROR_CHECK(dirp, NULL, "opendir");
// 切换工作目录为参数传入的目录
int ret = chdir(argv[1]);
ERROR_CHECK(ret, -1, "chdir");
// dirent 结构体指针,用于存放目录项信息
struct dirent *pdirent;
// 读取目录中的每个目录项
while ((pdirent = readdir(dirp)) != NULL) {
// 拼接文件路径,如果上面没有 chdir 切换目录,这里就需要做绝对路径的拼接操作
// char path[1024] = {0};
// sprintf(path, "%s%s%s", argv[1], "/", pdirent->d_name);
// 或者也可以用 strcpy 以及 strcat 函数进行复制拼接获取最终目录
// strcpy(path, argv[1]);
// strcat(path, "/");
// strcat(path, pdirent->d_name);
// 获取文件相关的信息
struct stat statbuf;
int ret = stat(
pdirent->d_name,
&statbuf); // 由于切换了工作目录,所以文件名就是路径名。如果没有切换,那么这里需要传参 path
ERROR_CHECK(ret, -1, "stat");
// 输出文件相关的信息
printf(
"%o %lu %u %u %lu %lu %s\n",
statbuf.st_mode, // 以八进制无符号输出,可以看到权限的数字表示法
statbuf.st_nlink, // 无符号长整型输出
statbuf.st_uid, // 无符号 int 输出
statbuf.st_gid, // 无符号 int 输出
statbuf.st_size, // 无符号长整型输出
statbuf.st_mtim
.tv_sec, // 时间戳打印秒数,以无符号长整型打印。这里还可以写 statbuf.st_mtime
pdirent->d_name); // 字符串打印
}
// 关闭目录流
closedir(dirp);
return 0;
}
再优化
获取文件权限信息: st_mode
成员的最后 9 位二进制数表示文件的权限。以下是如何根据 st_mode
判断文件权限的方法:
- 文件权限
777
的二进制表示为111 111 111
。 - 文件权限
775
的二进制表示为111 110 101
。 - 文件权限
664
的二进制表示为110 110 100
。
判断权限: 通过将 st_mode
与特定的位掩码进行按位与操作,可以判断文件的权限:
- 与
0400
(可读)进行按位与操作,结果非 0 表示拥有者具有读权限。 - 与
0200
(可写)进行按位与操作,结果非 0 表示拥有者具有写权限。 - 与
0100
(可执行)进行按位与操作,结果非 0 表示拥有者具有执行权限。
// 设置文件类型和权限字符串
void set_type_mode(mode_t mode, char *tm_str){
// 处理第一个字符,即文件类型
switch (mode & S_IFMT) {
case S_IFBLK: tm_str[0] = 'b'; break;
case S_IFCHR: tm_str[0] = 'c'; break;
case S_IFDIR: tm_str[0] = 'd'; break;
case S_IFIFO: tm_str[0] = 'p'; break;
case S_IFLNK: tm_str[0] = 'l'; break;
case S_IFREG: tm_str[0] = '-'; break;
case S_IFSOCK: tm_str[0] = 's'; break;
default: tm_str[0] = '?'; break;
}
// 处理后续九个字符,即文件的权限信息
// 设置拥有者的权限信息
tm_str[1] = (mode & 0400) ? 'r' : '-';
tm_str[2] = (mode & 0200) ? 'w' : '-';
tm_str[3] = (mode & 0100) ? 'x' : '-';
// 设置拥有者组的权限
tm_str[4] = (mode & 0040) ? 'r' : '-';
tm_str[5] = (mode & 0020) ? 'w' : '-';
tm_str[6] = (mode & 0010) ? 'x' : '-';
// 设置其他人的权限
tm_str[7] = (mode & 0004) ? 'r' : '-';
tm_str[8] = (mode & 0002) ? 'w' : '-';
tm_str[9] = (mode & 0001) ? 'x' : '-';
tm_str[10] = '\0'; // 确保字符串以 null 结尾
}
// 获取格式化的时间字符串
void set_time(time_t mtime, char *time_str){
// 由于tm结构体中存储的是月份的整数值,我们需要的是月份字符串,所以用一个字符串数组来存储月份字符串
const char month_arr[][10] = {
"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"
}; // tm结构体当中的月份范围是[0, 11],刚好可以适配这个数组
// 调用localtime函数,获取tm结构体指针
struct tm* st_tm = localtime(&mtime);
// 构建时间字符串,格式为:月份 天数 时:分
sprintf(time_str, "%s %2d %02d:%02d",
month_arr[st_tm->tm_mon],
st_tm->tm_mday,
st_tm->tm_hour,
st_tm->tm_min);
}
/* Usage: ./07_myls pathname 或 ./07_myls */
int main(int argc, char* argv[]) {
char* dir_name; // 存储目录名的指针
if(argc == 1) {
dir_name = "."; // 如果命令行参数没有提供要打印的目录,就打印当前工作目录
}
else if (argc == 2){
dir_name = argv[1]; // 否则使用提供的命令行参数作为待打印的目标
}else {
fprintf(stderr, "args num error!\n");
exit(1);
}
DIR* dirp = opendir(dir_name); // 打开指定的目录
ERROR_CHECK(dirp, NULL, "opendir");
// 改变工作目录到指定目录
int ret = chdir(dir_name);
ERROR_CHECK(ret, -1, "chdir");
struct dirent* pdirent;
// 遍历目录项
while ((pdirent = readdir(dirp)) != NULL) {
struct stat stat_buf;
// 获取目录项的详细信息
int ret = stat(pdirent->d_name, &stat_buf);
ERROR_CHECK(ret, -1, "stat");
char mode_str[1024] = { 0 }; // 保存文件类型和权限信息
set_type_mode(stat_buf.st_mode, mode_str); // 设置类型和权限
char time_str[1024] = { 0 }; // 保存格式化后的时间信息
/*
localtime需要传入时间戳描述
所以这里传参时可以写stat_buf.st_mtime(宏定义,更简写)
也可以写stat_buf.st_mtim.tv_sec(宏定义的原版代码)
但不能直接写stat_buf.st_mtim
原因上面已经讲过了!
*/
set_time(stat_buf.st_mtime, time_str); // 获取时间字符串
printf("%s %2lu %s %s %6lu %s %s\n",
mode_str, // 文件类型与权限
stat_buf.st_nlink, // 硬链接数,不足2个字符的在前面补空格
getpwuid(stat_buf.st_uid)->pw_name, // 拥有者名
getgrgid(stat_buf.st_gid)->gr_name, // 拥有者组名
stat_buf.st_size, // 文件大小,使用%4lu表示最少输出4个字符,若不足4个字符则在前面补空格
time_str, // 最后修改时间字符串
pdirent->d_name); // 文件名
}
// 关闭目录
closedir(dirp);
return 0;
}