本文描述了文件的属性,主要是struct stat结构体中的相关字段,比如文件所有者ID、文件所属组ID、块大小等。

其次详细描述了文件权限的相关内容,包括文件的基本权限、进程创建、读写文件的权限验证规则以及修改文件权限的相关接口等。

最后是文件系统简介,包含inode、目录项等,以及文件时间,创建删除以及读写目录等。

文件属性和类型

struct stat结构体是系统对文件属性的一个抽象,具体内容如下:

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
};

Ubuntu20.04中根据头文件/usr/include/x86_64-linux-gnu/bits/stat.h文件中的提示,需要定义__USE_GNU才可以使用st_atim, st_mtim, st_ctim参数。否则只能使用st_atime, st_mtime, st_ctime参数。

程序获取struct stat结构体的方式

#include <sys/stat.h>
int stat(const char* restrict pathname, struct stat *restrict buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
int fstatat(int dirfd, const char *restrict pathname, struct stat *restrict buf);
//成功返回0,失败返回-1

stat函数返回pathname参数指向的文件的属性结构体struct stat

fstat函数返回在描述符fd上打开的文件的有关信息。

lstat函数仅会对链接文件进行特殊处理,指向链接文件本身。其他的和stat函数一致。

#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

static void print_stat(struct stat* pFileStat)
{
    printf("st_mode: %X\n", pFileStat->st_mode);
    printf("st_inf: %lu\n", pFileStat->st_ino);
    printf("st_dev: %lu\n", pFileStat->st_dev);
    printf("st_rdev: %lu\n", pFileStat->st_rdev);
    printf("st_nlink: %lu\n", pFileStat->st_nlink);
    printf("st_uid: %d\n", pFileStat->st_uid);
    printf("st_gid: %d\n", pFileStat->st_gid);
    printf("st_size: %ld\n", pFileStat->st_size);
    printf("st_atime: %lu\n", pFileStat->st_atime);
    printf("st_mtime: %ld\n", pFileStat->st_mtime);
    printf("st_ctime: %lu\n", pFileStat->st_ctime);
    printf("st_blksize: %ld\n", pFileStat->st_blksize);
    printf("st_blocks: %ld\n", pFileStat->st_blocks);
}

int main()
{
    struct stat fileStat;
    if (stat("./test", &fileStat) == -1)
    {
        perror("stat failed");
        return 0;
    }
    print_stat(&fileStat);

    struct stat fileStat1;
    int fd = -1;
    if ((fd = open("./test", O_RDONLY)) == -1)
    {
        perror("open failed");
        return 0;
    }
    if (fstat(fd, &fileStat1) == -1)
    {
        perror("fstat failed");
        return 0;
    }
    printf("fileStat1:\n");
    print_stat(&fileStat1);
    close(fd);

    struct stat fileStat2;
    int fd1 = -1;
    if ((fd1 = open("./test-link", O_RDONLY)) == -1)
    {
        perror("open failed");
        return 0;
    }
    if (fstat(fd1, &fileStat2) == -1)
    {
        perror("fstat failed");
        return 0;
    }
    printf("fileStat2:\n");
    print_stat(&fileStat2);
    close(fd1);
    return 0;
}
$ ./stat_test 
st_mode: 81B4
st_inf: 1976724
st_dev: 2053
st_rdev: 0
st_nlink: 2
st_uid: 1000
st_gid: 1000
st_size: 0
st_atime: 1646486168
st_mtime: 1646486168
st_ctime: 1646488925
st_blksize: 4096
st_blocks: 0
fileStat1:
st_mode: 81B4
st_inf: 1976724
st_dev: 2053
st_rdev: 0
st_nlink: 2
st_uid: 1000
st_gid: 1000
st_size: 0
st_atime: 1646486168
st_mtime: 1646486168
st_ctime: 1646488925
st_blksize: 4096
st_blocks: 0
fileStat2:
st_mode: 81B4
st_inf: 1976724
st_dev: 2053
st_rdev: 0
st_nlink: 2 // 硬链接数量为2
st_uid: 1000
st_gid: 1000
st_size: 0
st_atime: 1646486168
st_mtime: 1646486168
st_ctime: 1646488925 // 文件状态时间和文件修改时间不同
st_blksize: 4096
st_blocks: 0

fstatat函数为一个相对于打开目录(dirfd指向的)的路径名返回文件统计信息。

flag参数控制是否跟随符号链接还是指向符号链接本身,当值为AT_SYMLINK_NOFLLOW时指向符号链接本身。默认情况下返回的是符号链接指向的文件。

如果dirfd参数是AT_FDCWD,并且pathname参数是一个相对路径名,fstatat会计算相对于当前目录的pathname参数。如果pathname是绝对路径,则fd参数被忽略。根据flag的取值不同,fstatat的作用和statlstat类似。

下面是fstatat函数的使用范例

#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define _XOPEN_SOURCE 700 /* Or define _POSIX_C_SOURCE >= 200809 */
static void print_stat(struct stat* pFileStat)
{
    printf("st_mode: %X\n", pFileStat->st_mode);
    printf("st_inf: %lu\n", pFileStat->st_ino);
    printf("st_dev: %lu\n", pFileStat->st_dev);
    printf("st_rdev: %lu\n", pFileStat->st_rdev);
    printf("st_nlink: %lu\n", pFileStat->st_nlink);
    printf("st_uid: %d\n", pFileStat->st_uid);
    printf("st_gid: %d\n", pFileStat->st_gid);
    printf("st_size: %ld\n", pFileStat->st_size);
    printf("st_atime: %lu\n", pFileStat->st_atime);
    printf("st_mtime: %ld\n", pFileStat->st_mtime);
    printf("st_ctime: %lu\n", pFileStat->st_ctime);
    printf("st_blksize: %ld\n", pFileStat->st_blksize);
    printf("st_blocks: %ld\n", pFileStat->st_blocks);
}
int main()
{
    int dirfd = open(".", O_RDONLY);
    if (dirfd == -1)
    {
        perror("open failed");
        return 0;
    }

    //以dirfd为基础相对目录访问文件
    printf("relative:\n");
    struct stat fileStat;
    if (fstatat(dirfd, "./test", &fileStat, AT_SYMLINK_NOFOLLOW) == -1)
    {
        perror("fstatat failed");
        return 0;
    }
    print_stat(&fileStat);
    close(dirfd);

    //以当前目录为基础的相对目录访问文件
    printf("relative1:\n");
    struct stat fileStat1;
    if (fstatat(AT_FDCWD, "test", &fileStat1, AT_SYMLINK_NOFOLLOW) == -1)
    {
        perror("fstatat failed");
        return 0;
    }
    print_stat(&fileStat1);

    //以绝对目录访问文件
    printf("absolute:\n");
    struct stat fileStat2;
    if (fstatat(AT_FDCWD, "/home/blduan/Documents/code_snippet/4_chapter/test",
            &fileStat2, AT_SYMLINK_NOFOLLOW) == -1)
    {
        perror("fstatat failed");
        return 0;
    }
    print_stat(&fileStat2);
    return 0;
}
//执行结果如下:通过3种方式都可以访问到指定文件
$ ./fstatat_test 
relative:
st_mode: 81B4
st_inf: 1976724
st_dev: 2053
st_rdev: 0
st_nlink: 2
st_uid: 1000
st_gid: 1000
st_size: 0
st_atime: 1646486168
st_mtime: 1646486168
st_ctime: 1646488925
st_blksize: 4096
st_blocks: 0
relative1:
st_mode: 81B4
st_inf: 1976724
st_dev: 2053
st_rdev: 0
st_nlink: 2
st_uid: 1000
st_gid: 1000
st_size: 0
st_atime: 1646486168
st_mtime: 1646486168
st_ctime: 1646488925
st_blksize: 4096
st_blocks: 0
absolute:
st_mode: 81B4
st_inf: 1976724
st_dev: 2053
st_rdev: 0
st_nlink: 2
st_uid: 1000
st_gid: 1000
st_size: 0
st_atime: 1646486168
st_mtime: 1646486168
st_ctime: 1646488925
st_blksize: 4096
st_blocks: 0

文件类型

文件类型说明
普通文件regular file最常用的文件类型,但是二进制可执行文件必须遵循一种标准化的格式
目录文件directory file这种文件中包含了其他文件的名字以及指向与这些文件有关信息的指针
块特殊文件block special file这种文件提供对设备带缓冲的访问,每次访问必须以固定长度为单位
字符特殊文件character special file这种类型文件提供对设备不带缓冲的访问,每次访问长度可变。系统中的设备分为以上两类
FIFO这种类型的文件用于进程间通信,也别成为命名管道
套接字socket用于网络通信,也可用于进程间的非网络通信
符号链接symbolic link这种类型的文件指向另一个文件

文件类型信息存储在文件属性结构体struct stat的成员st_mode中,可以用以下宏确定文件类型:

文件类型
S_ISREG()普通文件
S_ISDIR()目录文件
S_ISCHR()字符特殊文件
S_ISBLK()块特殊文件
S_ISFIFO()管道或FIFO
S_ISLNK()链接文件
S_ISSOCK()套接字

确定进程间通信的类型

文件类型
S_TYPEISMQ()消息队列
S_TYPEISSEM()信号量
S_TYPEISSHM()共享内存

下面选中一些文件查看其类型

#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

int main(int argc, char* argv[])
{
    struct stat fileStat;
    for (int i = 1; i < argc; i++)
    {
        printf("%s: ", argv[i]);
        if(lstat(argv[i], &fileStat)<0){
            perror("lstat failed");
            continue;
        }
        if(S_ISREG(fileStat.st_mode))
            printf("regular file\n");
        else if(S_ISDIR(fileStat.st_mode))
            printf("directory file\n");
        else if(S_ISCHR(fileStat.st_mode))
            printf("character special file\n");
        else if(S_ISBLK(fileStat.st_mode))
            printf("block special file\n");
        else if(S_ISFIFO(fileStat.st_mode))
            printf("FIFO\n");
        else if(S_ISLNK(fileStat.st_mode))
            printf("symbolic link\n");
        else if(S_ISSOCK(fileStat.st_mode))
            printf("socket\n");
        else
            printf("unknow mode\n");
    }
    return 0;
}
$ ./lstat_file_type_test /etc/passwd /etc/ /dev/log /dev/tty /dev/sr0 /dev/cdrom 
/etc/passwd: regular file
/etc/: directory file
/dev/log: symbolic link
/dev/tty: character special file
/dev/sr0: block special file
/dev/cdrom: symbolic link

文件进程用户权限

与进程有关的ID

ID来源及用途
实际用户ID登录时的用户,取自口令文件中的登录项
实际组ID登录用户所在的组
有效用户ID通常情况下和实际用户ID相同,但可以通过设置条件改变。有效用户ID和有效组ID决定文件访问权限
有效组ID同有效用户ID
附属组ID通常情况下为登录用户的所在的附属组。附属组ID也决定文件访问权限
保存的设置用户ID有效用户发生变化时产生,用于设置回原来的有效用户ID
保存的设置组ID同上

与文件有关的ID

每个文件都有所有者owner和所属组group,可以通过ls -l命令来查看。

文件的所有者和所属组的ID和登录用户的ID和组ID不一定相同。例如即便普通用户登录时,/etc/passwd文件的所有者和组所有者都属于root,ID始终都为0。

文件的所有者ID和组所有者ID保存在文件属性结构体struct stat中的st_uidst_gid字段中

设置用户ID位和设置组ID位保存在文件的st_mode值中,可以使用宏S_ISUIDS_ISGID测试

正常情况下,当执行一个程序文件时,进程的有效用户ID和有效组ID就是实际用户ID和实际组ID。

但是当对程序文件设置了一个标志(即设置用户ID位,其含义是当执行此文件时,进程的有效用户ID设置为文件所有者的用户ID)并且当程序文件的所有者为root时,进程的有效用户ID就为root了,也就有了超级用户权限。

例如/usr/bin/passwd文件的所有者和组所有者为root,当普通用户执行该文件时,passwd进程就有了root权限,可以将用户的新口令写入到文件/etc/shadow中。

root权限的进程拥有文件的所有权限,但是想要执行文件的话,该文件必须要至少有一个可执行权限,表明它是可执行的。

示例一:判断设置用户ID位

下面的示例用于判断可执行文件是否设置了设置用户ID位和设置组ID位:

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

int main(int argc, char* argv[])
{
    struct stat fileProps;

    for (int i = 1; i < argc; i++)
    {
        // lstat函数对于链接返回其自身,而非链接指向的文件
        if (lstat(argv[i], &fileProps) < 0)
        {
            perror("lstat failed");
            continue;
        }
        // 设置了设置用户ID位,进程以文件所有者的用户ID作为有效用户ID
        if (S_ISUID & fileProps.st_mode)
        {
            printf("%s: set user id\n", argv[i]);
        }
        if (S_ISGID & fileProps.st_mode)// 有效组ID同理
        {
            printf("%s: set group id\n", argv[i]);
        }
    }
    return 0;
}
$ ls -l /usr/bin/passwd ./test
-rwSrwSr-- 1 blduan blduan     0 Mar  6 05:38 ./test
-rwsr-xr-x 1 root   root   68208 Jul 14  2021 /usr/bin/passwd
$./lstat_file_user_test /usr/bin/passwd ./test
/usr/bin/passwd: set user id
./test: set user id
./test: set group id

从结果中可以看到,/usr/bin/passwd设置了设置用户ID位,测试程序test既设置了设置用户ID位,也设置设置组ID位。

示例二:进程有效用户ID变化

下面的示例中主要展示设置了设置用户ID位或设置组ID位之后,进程的有效用户ID的变化:

# 普通用户
$ ls -l process_euid_test
-rwxrwxr-x 1 blduan blduan 16800 Mar  6 06:12 process_euid_test
$ ./process_euid_test 
euid=1000
egid=1000
# 修改所有者
$ sudo chown root process_euid_test
$ ls -l process_euid_test
-rwxrwxr-x 1 root blduan 16800 Mar  6 06:12 process_euid_test
# 设置设置用户ID位
$ sudo chmod u+s process_euid_test
$ ls -l process_euid_test
-rwsrwxr-x 1 root blduan 16800 Mar  6 06:12 process_euid_test
$ ./process_euid_test 
euid=0
egid=1000
# 修改组所有者和设置设置组ID位
$ sudo chgrp root process_euid_test
$ sudo chmod g+s process_euid_test
$ ls -l process_euid_test
-rwsrwsr-x 1 root root 16800 Mar  6 06:12 process_euid_test
$ ./process_euid_test 
euid=0
egid=0

进程源码如下:

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("euid=%d\n", geteuid());
    printf("egid=%d\n", getegid());
    return 0;
}

从结果中可以看出,当更改测试程序的所有者以及所属组为root并设置了设置用户ID位以及设置组ID位之后,进程的有效用户ID和有效组ID都变为root用户对应的ID。

文件的所有权限

访问权限位含义
S_IRUSR所有者读
S_IWUSR所有者写
S_IXUSR所有者执行
S_IRGRP组读
S_IWGRP组写
S_IXGRP组执行
S_IROTH其他读
S_IWOTH其他写
S_IXOTH其他执行

和文件权限相关的部分规则如下:

  1. 用绝对路径打开任意类型的文件时,必须对该路径中包含的各级目录具有执行权限。执行权限也常被成为搜索位。例如要打开/usr/include/stdio.h,则必须对目录/、/usr、/usr/include都有执行权限。对目录的读权限和执行权限的意义是不同的。 读权限允许获取该目录中所有文件名的列表。
  2. 对文件的读权限决定能否打开文件进行读操作。与open函数的O_RDONLYO_RDWR标志有关。
  3. 对文件的写权限决定能否打开文件进行写操作。与open函数的O_WRONLYORDWR标志有关。
  4. open对文件指定O_TRUNC标志,则必须对文件具有写权限。
  5. 在目录中创建新文件,必须对目录有写和执行权限
  6. 删除现有文件,必须对目录有写和执行权限。
  7. 使用exec族函数执行一个文件时,必须对该文件具有执行权限,并且该文件必须是普通文件。

进程访问文件之权限判断规则

内核每次打开、创建或删除一个文件时,内核都会进行文件访问权限测试,测试涉及到文件的所有者(st_uidst_gid)、进程的有效ID(有效用户ID和有效组ID)以及进程的附属组ID。内核测试的具体流程如下:

  1. 若进程的有效用户ID是0(超级用户),则允许访问。
  2. 若进程的有效用户ID等于文件的所有者ID(进程拥有此文件),那么如果文件所有者适当的访问权限位(所有者的读写执行权限)被设置,则允许访问;否则拒绝访问。
  3. 若进程的有效组ID或进程的附属组ID之一等于文件的组ID,那么如果文件的组适当的访问权限位被设置,则允许访问;否则拒绝访问。
  4. 若文件其他用户适当的访问权限位被设置,则允许访问;否则拒绝访问。

注意:上面4补依次执行,如果进程拥有此文件(满足第2步),则按文件所有者访问权限进行检测,不再查看组访问权限(第3步)。同理,如果进程不拥有此文件,但进程的有效组或附属组与文件所属组相等,则按组的访问权限进行检测,而不查看其他用户的访问权限

测试流程如下图:

权限测试函数access()和faccessat()

#include <unistd.h>

int access(const char *pathname int mode);
int faccessat(int fd, const char *pathname,  int mode, int flag);
// 成功返回0, 失败返回-1

accessfaccessat函数默认是按照实际用户ID和实际组ID进行权限测试的,而不是以进程的有效用户ID和有效组ID。

pathname为绝对路径和fd参数值为AT_FDCWD并且pathname参数为相对路径时,accessfaccessat函数作用一致。

flag参数可以改变faccessat函数的行为,如果flag设置为AT_EACCESS访问检查的就是进程的有效用户ID和有效组ID。

下面验证accessfaccessat函数的作用:

# access_test进程文件的所有者是blduan,并且设置了设置用户ID和设置组ID位,因此有效用户ID和有效组ID是用户blduan的ID。
# 但是登录用户是test,所以进程的实际用户ID和实际组ID是用户test的ID。
test@ubuntu:/home/blduan/Documents/code_snippet/4_chapter$ ls -l access_test
-rwsrwsr-x 1 blduan blduan 17056 Mar  7 07:16 access_test
# 用户blduan具有读写执行权限,用户test仅有读权限。
test@ubuntu:/home/blduan/Documents/code_snippet/4_chapter$ ls -l access_test.file 
-rwxrw-r-- 1 blduan blduan 0 Mar  7 06:28 access_test.file
# 测试执行结果和预期一致。
test@ubuntu:/home/blduan/Documents/code_snippet/4_chapter$ ./access_test 
uid: 1001; gid: 1001
euid: 1000; egid: 1000
# 实际用户权限
exist access ok
read access ok
access W_OK failed: Permission denied
access X_OK failed: Permission denied
# 有效用户权限
read faccessat ok
write faccessat ok
execute faccessat ok

测试代码如下:

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

int main()
{
    printf("uid: %d; gid: %d\n", getuid(), getgid());
    printf("euid: %d; egid: %d\n", geteuid(), getegid());

    //实际用户验证权限
    if (access("./access_test.file", F_OK) < 0)
    {
        perror("access F_OK failed");
    }
    else
    {
        printf("exist access ok\n");
    }

    if (access("./access_test.file", R_OK) < 0)
    {
        perror("access R_OK failed");
    }
    else
    {
        printf("read access ok\n");
    }

    if (access("./access_test.file", W_OK) < 0)
    {
        perror("access W_OK failed");
    }
    else
    {
        printf("write access ok\n");
    }

    if (access("./access_test.file", X_OK) < 0)
    {
        perror("access X_OK failed");
    }
    else
    {
        printf("execute access ok\n");
    }

    // faccessat函数当flag=AT_EACCESS时,采用的是进程的有效用户ID和有效组ID
    if (faccessat(AT_FDCWD, "./access_test.file", R_OK, AT_EACCESS) < 0)
    {
        perror("faccessat R_OK failed");
    }
    else
    {
        printf("read faccessat ok\n");
    }

    if (faccessat(AT_FDCWD, "./access_test.file", W_OK, AT_EACCESS) < 0)
    {
        perror("faccessat W_OK failed");
    }
    else
    {
        printf("write faccessat ok\n");
    }

    if (faccessat(AT_FDCWD, "./access_test.file", X_OK, AT_EACCESS) < 0)
    {
        perror("faccessat X_OK failed");
    }
    else
    {
        printf("execute faccessat ok\n");
    }
    return 0;
}

权限设置函数umask()

umask函数为进程设置文件模式创建 屏蔽字,并返回之前的值。当进程创建文件时,在文件模式创建屏蔽字中为1的位,在文件mode中的相应位一定被关闭。

#include <sys/stat.h>
mode_t umask(mode_t cmask);
// 返回之前的文件模式创建屏蔽字

下面验证umask函数的功能:

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

int main()
{
    // 屏蔽其他用户的读写执行权限,此时创建的文件的权限应是770
    umask(S_IROTH | S_IWOTH | S_IXOTH);
    // open函数创建文件时,设置了所有权限即777,但是umask屏蔽之后也应该是770
    if (open("./umask_test.file", O_WRONLY | O_CREAT | O_TRUNC,
            S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP |
                S_IROTH | S_IWOTH | S_IXOTH) < 0)
    {
        perror("open failed");
        return 0;
    }
    else
    {
        printf("create file ok\n");
    }
    return 0;
}

查看执行结果如下:

$ ./umask_test 
create file ok
$ ls -l umask_test.file 
-rwxrwx--- 1 blduan blduan 0 Mar  7 18:04 umask_test.file

默认的umask值通常在登录时,由shell的启动文件进行设置一次,值为002

系统不允许创建文件时,创建的文件拥有执行权限,因此文件的权限最大值为666,但是umask值为002,因此默认情况下文件的权限为644。同理目录的权限为775

$ umask
000
$ ls -l .
drwxrwxr-x 2 blduan blduan  4096 Mar  7 18:16 umask
-rw-rw-r-- 1 blduan blduan     0 Mar  7 18:16 umask.test
屏蔽位含义
0400用户读
0200用户写
0100用户执行
0040组读
0020组写
0010组执行
0004其他读
0002其他写
0001其他执行

权限修改函数chmod()、fchmod()和fchmodat()

#include <sys/stat.h>
int chmod(const char* pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
int fchmodat(int dirfd, const char* pathname, mode_t mode, int flag);
// 成功返回0, 失败返回-1

这几个函数之间的区别和statfstat以及fstatat之间的区别是相同的。根据man文档提示fchmodat现在还不支持flag参数为AT_SYMLINK_NOFOLLOW的情况。

为了改变一个文件的权限位,进程的有效用户ID必须等于文件的所有者ID,或者进程必须拥有超级用户权限。

chmod系列函数仅会修改i节点最近一次被更改的时间,ls命令显示的是文件内容被修改的时间。因此调用了chmod之后,ls输出结果不变。

同时指出文件属性结构体stat中的文件时间字段来自与i节点数据。

参数mode的取值范围如下:

mode说明
S_ISUID设置用户ID位
S_ISGID设置组ID位
S_ISVTX保存正文
S_IRWXU所有者读写执行
S_IRUSR所有者读
S_IWUSR所有者写
S_IXUSR所有者执行
S_IRWXG组读写执行
S_IRGRP组读
S_IWGRP组写
S_IXGRP组执行
S_IRWXO其他读写执行
S_IROTH其他读
S_IWOTH其他写
S_IXOTH其他执行

下面验证chmod函数改变文件权限:

$ ls -l chmod_test.file 
-rwxrwxrwx 1 blduan blduan 0 Mar  9 07:30 chmod_test.file
$ ./chmod_test
euid=1000, egid=1000
chmod 700 ok
fchmod 770 ok
fchmodat 777 ok
$ ./chmod_test
euid=1001, egid=1001
chmod failed: Operation not permitted
fchmod failed: Operation not permitted
fchmodat failed: Operation not permitted
$ sudo ./chmod_test
euid=0, egid=0
chmod 700 ok
fchmod 770 ok
fchmodat 777 ok

测试代码如下:

#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

int main()
{
    // 分别验证进程的有效用户ID是文件所有者的ID、不是文件所有者的ID以及是否具有超级权限
    printf("euid=%d, egid=%d\n", geteuid(), getegid());
    if (chmod("./chmod_test.file", S_IRWXU) < 0)
    {
        perror("chmod failed");
    }
    else
    {
        printf("chmod 700 ok\n");
    }

    int fd = open("./chmod_test.file", O_RDONLY);
    if (fd < 0)
    {
        perror("open failed");
    }
    if (fchmod(fd, S_IRWXU | S_IRWXG) < 0)
        perror("fchmod failed");
    else
        printf("fchmod 770 ok\n");
    close(fd);

    if (fchmodat(
            AT_FDCWD, "./chmod_test.file", S_IRWXU | S_IRWXG | S_IRWXO, 0) < 0)
        perror("fchmodat failed");
    else
        printf("fchmodat 777 ok\n");

    return 0;
}

粘着位

根据之前,想要删除一个文件,只要对文件所在的目录具有写和执行权限,对文件不需要任何权限。

这样的话,对于共享目录(比如/tmp),是所有用户都拥有读写执行权限。那么这就会存在一个问题,当前用户可以删除其他用户创建的文件。

粘着位就是为了解决这个问题,如果对一个目录设置了粘着位,那么用户想要删除或重命名该目录下的文件需要具备以下条件之一才可以:

  • 该文件的所有者
  • 目录的拥有者
  • 是超级用户

测试用例如下:

# 创建共享目录share_dir
blduan@ubuntu:~$ mkdir share_dir
# 目录的所有者是blduan
blduan@ubuntu:~$ ls -l
drwxrwxr-x 2 blduan blduan 4096 Mar 10 06:41 share_dir
# 共享目录一般所有用户都有读写执行权限
blduan@ubuntu:~$ ls -l
drwxrwxrwx 2 blduan blduan 4096 Mar 10 06:41 share_dir
# 共享目录中创建测试文件test.file
blduan@ubuntu:~/share_dir$ touch test.file
# test.file的所有者是blduan
blduan@ubuntu:~/share_dir$ ls -l
total 0
-rw-rw-r-- 1 blduan blduan 0 Mar 10 06:42 test.file
# 尝试用其他用户test删除文件test.file
test@ubuntu:/home/blduan/share_dir$ ls -l
total 0
-rw-rw-r-- 1 blduan blduan 0 Mar 10 06:42 test.file
test@ubuntu:/home/blduan/share_dir$ rm -rf test.file 
# 可以看到test用户对share_dir具有读写执行权限,可以删除用户blduan创建的文件test.file
test@ubuntu:/home/blduan/share_dir$ ls -l
total 0
# 为了解决这个问题,对share_dir设置粘着位
blduan@ubuntu:~$ chmod o+t share_dir
blduan@ubuntu:~$ ls -l
drwxrwxrwt 2 blduan blduan 4096 Mar 10 06:44 share_dir
# 设置完粘着位,重新在共享目录中创建测试文件test.file1
blduan@ubuntu:~/share_dir$ touch test.file1
blduan@ubuntu:~/share_dir$ ls -l test.file1 
-rw-rw-r-- 1 blduan blduan 0 Mar 10 06:46 test.file1
# 接下来再次使用用户test删除文件
test@ubuntu:/home/blduan/share_dir$ ls -l
total 0
-rw-rw-r-- 1 blduan blduan 0 Mar 10 06:46 test.file1
# 显然设置了粘着位之后,用户test没有权限删除用户blduan的文件
test@ubuntu:/home/blduan/share_dir$ rm -rf test.file1 
rm: cannot remove 'test.file1': Operation not permitted

文件用户更改函数chown()、fchown()、fchownat()和lchown()

chown函数用来更改文件的用户id和组id。

#include <unistd.h>

int chown(const char* pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int fchownat(int dirfd, const char* pathname, uid_t owner, gid_t group, int flag);
int lchown(const char* pathname, uid_t owner, gid_t group);
// 成功返回0,失败返回-1

如果参数ownergroup中任意一个位-1,则对应的ID不变。

chown系列函数和stat系列函数的区别一致。

常规情况下,仅有超级用户拥有才能更改一个文件的所有者;

如果这些函数由非超级权限用户进程调用,则成功返回时,文件的设置用户ID位和设置组ID位会被清楚。

下面是对chown函数的测试结果

# 创建测试文件chown_test.file
blduan@ubuntu:~$ touch chown_test.file
# 查看文件的所有者为blduan
blduan@ubuntu:~$ ls -l chown_test.file 
-rw-rw-r-- 1 blduan blduan 0 Mar 11 06:11 chown_test.file
# 进程的有效用户ID为1001,等于实际用户ID,即blduan用户。
blduan@ubuntu:~$ ./chown_test 
euid=1000, egid=1000
chown failed
: Operation not permitted
# 文件所有者未变
blduan@ubuntu:~$ ls -l chown_test.file 
-rw-rw-r-- 1 blduan blduan 0 Mar 11 06:11 chown_test.file
# 以超级权限进程修改文件所有者
blduan@ubuntu:~$ sudo ./chown_test 
euid=0, egid=0
chown ok
# 文件所有者发生改变
blduan@ubuntu:~$ ls -l chown_test.file 
-rw-rw-r-- 1 test blduan 0 Mar 11 06:11 chown_test.file

测试代码如下:

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("euid=%d, egid=%d\n", geteuid(), getegid());
    if (chown("./chown_test.file", 1001, -1) < 0)
    {
        perror("chown failed\n");
    }
    else
    {
        printf("chown ok\n");
    }
    return 0;
}

文件操作

文件长度和空洞

文件属性结构体struct stat中的st_size字段表示以字节为单位的文件的长度。此字段仅对普通文件、目录以及符号链接有效。

普通文件长度可以是0,读文件时会得到文件结束EOF提示。

目录文件长度通常是一个数(16或512)的整数倍。

符号链接文件长度是文件名中的实际字节数。

下面验证文件长度结果

blduan@ubuntu:~$ ./file_length_verify ./file_length_test.file ./file_length_test.link ./file_length_test_dir
./file_length_test.file: length=11 # 长度为文件内容实际长度
./file_length_test.link: length=21 # 长度为指向的文件名的长度
./file_length_test_dir: length=4096 # 长度为16或512的整数倍
blduan@ubuntu:~$ ls -l file_length_test.file 
-rw-rw-r-- 1 blduan blduan 11 Mar 11 06:58 file_length_test.file
blduan@ubuntu:~$ ls -l file_length_test.link 
lrwxrwxrwx 1 blduan blduan 21 Mar 11 06:54 file_length_test.link -> file_length_test.file
blduan@ubuntu:~$ ls -l ./
drwxrwxr-x 2 blduan blduan  4096 Mar 11 06:54 file_length_test_dir

下面是验证代码:

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

int main(int argc, char* argv[])
{
    struct stat fileProps;

    for (int i = 1; i < argc; i++)
    {
        if (lstat(argv[i], &fileProps) < 0)
        {
            perror("lstat failed");
            continue;
        }

        printf("%s: ", argv[i]);
        printf("length=%ld\n", fileProps.st_size);
    }
}

文件空洞

文件空洞的产生原因是设置的偏移量大于文件的实际长度,并且写入了数据。

Ubuntu20.04上du命令报告的1024字节块的数量,struct stat结构体中的st_blocks计算的是512字节块的数量。ls -s命令和du一致。

文件截断truncate()

下面函数支持在文件尾端截断一部分数据。将文件截断为0,可以通过open函数以及O_TRUNC标志实现。

#include <unistd.h>

int truncate(const char* pathname, off_t length);
int ftruncate(int fd, off_t length);
// 成功返回0, 失败返回-1

如果length参数大于文件长度,则会给文件创建空洞。

下面测试该函数功能

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
    //扩展文件生成文件空洞
    if (truncate("./truncate_test.file", 100000) < 0)
        perror("truncate failed");
    else
        printf("truncate ok\n");
    //清空文件
    int fd = open("./truncate_test.file1", O_WRONLY);
    if (fd < 0)
    {
        perror("open failed");
        return 0;
    }
    else
    {
        printf("open ok\n");
    }
    if (ftruncate(fd, 0) < 0)
        perror("ftruncate failed");
    else
        printf("ftruncate ok\n");
    close(fd);
}

测试结果如下:

blduan@ubuntu:~$ ls -l truncate_test.file*
-rw-rw-r-- 1 blduan blduan 14 Mar 12 06:41 truncate_test.file
-rw-rw-r-- 1 blduan blduan 11 Mar 12 06:41 truncate_test.file1
blduan@ubuntu:~$ ./truncate_test 
truncate ok
open ok
ftruncate ok
blduan@ubuntu:~$ ls -l truncate_test.file*
-rw-rw-r-- 1 blduan blduan 100000 Mar 12 06:42 truncate_test.file
-rw-rw-r-- 1 blduan blduan      0 Mar 12 06:42 truncate_test.file1

重命名

文件系统简介

磁盘分为一个或多个分区。每个分区都包含一个文件系统。

UFS(文件系统)由自举块、超级块以及多个柱面组组成。柱面组中包含超级块副本、配置信息、i节点位图、块位图以及i节点数组和数据块数组。i节点数组由一些列i节点组成,数据块数组由一些列数据块和目录项组成。

如下图所示:

由此可见文件系统中,一个文件由目录项、inode和数据块三部分组成:

  • 目录项: 包含inode节点号和文件名。
  • Inode: 文件索引节点,包含文件基础信息以及指向数据块的指针。
  • 数据块: 包含文件的具体内容。

inode

硬盘的最小存储单位为扇区,大小为512字节。操作系统如果以扇区为单位读取文件,效率太低。而是以块为单位读取文件,块的大小为扇区的整数倍,最常见的大小为4096字节。

文件数据以块为单位存储在硬盘中。还需要一个地方存储文件的基本信息,比如文件的创建者、时间信息、文件大小等,这种存储文件元信息的区域叫做inode,即索引节点。

存储inode也会消耗磁盘空间,所以硬盘格式化时,操作系统自动将磁盘分为两个区域。一个是数据区,存放文件数据;另一个是inode区,存放inode所包含的信息。

每个inode节点的大小一般为128字节或256字节。inode节点的总数,在格式化时就已经确定,一般为没1KB就设置一个inode。

下面查看ext4文件系统的inode大小

blduan@ubuntu:~$ sudo dumpe2fs -h /dev/sda5 | grep "Inode size"
dumpe2fs 1.45.5 (07-Jan-2020)
Inode size:	          256

由于每个文件都必须有一个inode,因此就有可能发生,磁盘空间还有但是inode已经用光的情况。

每个inode都有一个号码,操作系统用inode号码来识别不同的文件。查看当前文件的inode编号如下:

blduan@ubuntu:~$ ls -i stat_test
1973604 stat_test
blduan@ubuntu:~$ stat stat_test
  File: stat_test
  Size: 17160     	Blocks: 40         IO Block: 4096   regular file
Device: 805h/2053d	Inode: 1973604     Links: 1
Access: (0775/-rwxrwxr-x)  Uid: ( 1000/  blduan)   Gid: ( 1000/  blduan)
Access: 2022-03-12 18:37:44.124404379 -0800
Modify: 2022-03-07 07:16:59.342125333 -0800
Change: 2022-03-07 07:16:59.342125333 -0800
 Birth: -

inode包含的文件相关信息主要有文件类型、文件访问权限位、文件长度和指向文件的指针。stat结构的大多信息都取自i节点。

目录项

Linux系统中,目录也是一种文件。打开目录,实际上就是打开目录文件。

目录文件的结构非常简单,就是一系列目录项的列表。每个目录项由两部分组成:所包含文件的文件名、该文件名对应的inode编号。

下面可以查看目录文件中包含了哪些inode信息

# test-link目录文件中包含一条目录项为test。
blduan@ubuntu:~$ ls -i test-link/
1973383 test

硬链接和符号链接

硬链接

一般情况下,文件名和inode编号是一一对应的,每个inode编号对应一个文件名。

但是Linux系统允许多个文件名指向同一个inode编号。这意味着可以通过不同的文件名访问同样的内容。

对文件内容的修改会影响到所有文件名,但是删除一个文件名,不会影响另一个文件名的访问,这种情况就是硬链接。

可以使用命令ln source_file target_file创建硬链接。

创建完成之后,会在数据块中创建一条新的目录项,目录项中的指向inode编号和源文件的相同,都指向同一个inode。inode信息中有一项为链接数,记录指向该inode的文件名总数,此时会+1。

创建目录时,默认会生成两个目录项:...。前者的inode编号就是当前目录的inode编号,等同于当前目录的硬链接;后者的inode编号就是当前目录的父目录的inode编号,等同于父目录的硬链接。因此任何一个目录的硬链接数总是等于2(父目录对其的硬链接和当前目录下.的硬链接)+子目录的总数。

叶目录(不包含子目录的目录)至少存在2个硬链接,一个是父目录对其的硬链接,一个是该目录下的.产生的硬链接。

父目录至少存在3个硬链接,一个是其父目录对其的硬链接,一个是其目录中存在的.,还有就是其子目录中的..

下面的函数可以用来创建硬链接:

#include <unistd.h>

int link(const char *oldpath, const char *newpath);

#include <fcntl.h>           /* Definition of AT_* constants */
#include <unistd.h>

int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, int flags);

// 成功返回0, 失败返回-1

这两个文件创建一个新的目录项newpath,引用现有文件oldpath。如果newpath存在,则返回出错。newpath路径名中的目录必须存在。

linkat函数的olddirfdnewdirfd参数是当使用相对目录时给出参考位置的,如果值为AT_FDCWD则计算当前工作目录的相对位置。

linkat函数的flags和其他函数的一致,都是根据AT_SYMLINK_FOLLOW标志来决定是链接本身还是链接指向的文件。

创建目录项和增加链接计数应当是一个原子操作。

下面验证link函数功能

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
    if (link("./link_test.file", "./link_test.lnk") < 0)
        perror("link failed");
    else
        printf("link ok\n");

    int olddirfd = open(".", O_RDONLY);
    int newdirfd = open(".", O_RDONLY);

    if (linkat(olddirfd, "./link_test.file1", newdirfd, "./link/link_test1.lnk",
            AT_SYMLINK_FOLLOW) < 0)
        perror("linkat error");
    else
        printf("linkat ok\n");
    close(olddirfd);
    close(newdirfd);
}

执行结果如下:

blduan@ubuntu:~$ ./link_test 
link ok
linkat ok
blduan@ubuntu:~$ ls -i link_test.file link_test.lnk
1973331 link_test.file  1973331 link_test.lnk
blduan@ubuntu:~$ ls -i link_test.file1 ./link/link_test1.lnk 
1973391 ./link/link_test1.lnk  1973391 link_test.file1

blduan@ubuntu:~$ stat link_test.file1 
  File: link_test.file1
  Size: 0         	Blocks: 0          IO Block: 4096   regular empty file
Device: 805h/2053d	Inode: 1973391     Links: 2
Access: (0664/-rw-rw-r--)  Uid: ( 1000/  blduan)   Gid: ( 1000/  blduan)
Access: 2022-03-13 03:26:33.572857628 -0700
Modify: 2022-03-13 03:26:33.572857628 -0700
Change: 2022-03-13 03:27:10.688749607 -0700
 Birth: -

删除一个现有的目录项可以调用unlink函数:

#include <unistd.h>

int unlink(const char* pathname);
int unlinkat(int dirfd, const char *pathname, int flag);
// 成功返回0, 失败返回-1

这两个函数删除目录项,同时使pathname指向的文件的链接数-1。

为了解除对文件的链接,必须对包含该目录项的目录具有写和执行权限。

如果对该目录设置了粘着位,则必须对该目录具有写权限,并且具备以下三个条件之一

  • 拥有该文件
  • 拥有该目录
  • 超级用户权限

如果pathname为符号链接,unlink函数删除该符号链接,而不是符号链接指向的文件。

只有当文件的链接计数为0时,文件内容才可被删除。只要有进程打开了文件,其内容也不可被删除。

关闭一个文件时,内核首先去检查打开该文件的进程个数;如果这个计数达到0,内核再去检查其链接计数;如果计数也是0,那么内核就会删除该文件的内容。

下面验证内核这个功能:

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
    int fd = open("./unlink_test.file", O_RDWR);
    if (fd < 0)
        perror("open failed");
    else
        printf("open ok\n");

    if (unlink("./unlink_test.file") < 0)
        perror("unlink failed");
    else
        printf("unlink ok\n");
    sleep(15);
    printf("done\n");
    return 0;
}

测试结果如下

# 查看测试文件unlink_test.file的大小为16394
blduan@ubuntu:~$ ls -l unlink_test.file 
-rw-rw-r-- 1 blduan blduan 16394 Mar 13 03:47 unlink_test.file
# unlink_test.file大小占20个1k字节块
blduan@ubuntu:~$ du -s unlink_test.file 
20	unlink_test.file
# 下面可以看到已使用的1k字节块数为13456940
blduan@ubuntu:~$ df ~
Filesystem     1K-blocks     Used Available Use% Mounted on
/dev/sda5       50824704 13456940  34756308  28% /
blduan@ubuntu:~$ ./unlink_test &
[1] 13626
blduan@ubuntu:~$ open ok
unlink ok

# unlink接触链接之后目录项已经被删除了,但是文件内容由于进程没有关闭文件还存在,因为并没有释放1k字节块
blduan@ubuntu:~$ ls -l unlink_test.file 
ls: cannot access 'unlink_test.file': No such file or directory
blduan@ubuntu:~$ df ~
Filesystem     1K-blocks     Used Available Use% Mounted on
/dev/sda5       50824704 13456940  34756308  28% /
blduan@ubuntu:~$ done

[1]+  Done                    ./unlink_test、
# 进程退出时关闭了打开的文件,内核同时释放了文件内容
blduan@ubuntu:~$ df ~
Filesystem     1K-blocks     Used Available Use% Mounted on
/dev/sda5       50824704 13456920  34756328  28% /

unlink的这种特性可以使即使进程中途崩溃,其所创建的临时文件也不会遗留下来

remove函数也可以解除对一个文件或目录的链接。对于文件和unlink函数的功能一致,对于目录和rmdir相同。

#include <unistd.h>
int remove(const char* pathname);
// 成功返回0, 失败返回-1

符号链接

符号链接是对一个文件的间接引用,和硬链接不同,硬链接是直接指向文件的i节点。

符号链接文件拥有自己的目录项、i节点以及数据块。只是数据块中的文件内容是目标文件的路径名。

符号链接是为了避免硬链接的以下限制:

  • 硬链接通常要求链接和文件存在同一文件系统中。
  • 只有超级用户才能创建目录的硬链接。(Ubuntu20.04不允许对目录创建硬链接)

对符号链接以及它指向何种对象并没有文件系统限制,任何用户都可以创建指向目录的符号链接。

当使用以名字引用文件的函数时,应当了解该函数是否处理符号链接。支持符号链接的函数默认会跟随符号链接到指向的文件。

对目录的符号链接可能会在文件系统中引入循环,但是ls命令是不会跟随符号链接的,同时也有unlink函数也不跟随符号链接,可以很方便的用来删除符号链接。

可以使用symlinksymlinkat函数创建符号链接:

#include <unistd.h>

int symlink(const char* actualpath, const char* sympath);
int symlinkat(const char* actualpath, int fd, const char* sympath);
// 成功返回0 失败返回-1

因为open函数会跟踪符号链接,因此提供下面两个函数打开链接本身,并读取该链接中的名字。

#include <unistd.h>

ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);

#include <fcntl.h>           /* Definition of AT_* constants */
#include <unistd.h>

ssize_t readlinkat(int dirfd, const char *pathname, char *buf, size_t bufsiz);

这两个函数组合了open, read和close的所有操作,如果函数执行成功返回读入buf的字节数。

下面验证这两个函数功能:

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
    if (symlink("./symlink_test.file", "./symlink_test.link") < 0)
        perror("symlink error");
    else
        printf("symlink ok\n");

    char buf[256] = {0};
    if (readlink("./symlink_test.link", buf, sizeof(buf)) < 0)
        perror("readlink failed");
    else
        printf("readlink ok, content is %s\n", buf);
    return 0;
}

执行结果如下所示:

blduan@ubuntu:~$ ls -l symlink_test.file 
-rw-rw-r-- 1 blduan blduan 0 Mar 13 06:48 symlink_test.file
# 链接文件的内容是其指向的目标文件的路径名
blduan@ubuntu:~$ ./symlink_test 
symlink ok
readlink ok, content is ./symlink_test.file
blduan@ubuntu:~$ ls -l symlink_test.link 
lrwxrwxrwx 1 blduan blduan 19 Mar 13 06:48 symlink_test.link -> ./symlink_test.file

文件时间

文件的属性结构体对每个文件维护3个时间字段。

字段说明例子ls选项
st_atime文件数据的最后访问时间read-u
st_mtime文件数据的最后修改时间write默认
st_ctimei节点状态的最后修改时间chmod、chown-c

很多操作都会影响到i节点,比如更改文件的访问权限、更改用户ID、更改链接数,这些操作不会影响文件数据。

系统并不维护对i节点的最后访问时间,比如accessstat函数并不会更改3个时间中的任意一个。

目录是包含目录项(文件名和i节点编号)的文件,增加、删除和修改目录项会影响到它所在目录相关的3个时间。

下面是各种函数对访问、修改和状态更改时间的作用:


文件的访问和修改时间可以使用函数futimensutimensat来修改

#include <sys/stat.h>
int futimens(int fd, const struct timespec times[2]);
int utimensat(int dirfd, const char* path, const struct timespec times[2], int flag);
// 成功返回0, 失败返回-1

times数组的第一个元素表示访问时间,第二个元素表示修改时间。

如果timespec结构之一的tv_nsec字段具有特殊值UTIME_NOW,则相应的文件时间戳设置为当前时间。

如果timespec结构之一的tv_nsec字段具有特殊值UTIME_OMIT,则相应的文件时间戳保持不变。

在这两种情况下,相应的tv_sec的值将被忽略。

权限检查

要将两个文件时间戳都设置为当前时间(即times为 NULL,或者两个tv_nsec字段都指定 UTIME_NOW),必须具有以下条件之一:

  1. 进程必须对文件具有写访问权、进程的有效用户ID必须与文件的所有者匹配
  2. 进程具有超级用户权限

tv_nsec的值为UTIME_OMIT时,文件的相应时间戳不变,并且不会进行权限检查。

下面测试修改文件时间:

#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>

int main(int argc, char* argv[])
{
    int i;
    int fd;
    struct stat statbuf;
    struct timespec times[2];
    for (i = 1; i < argc; i++)
    {
        if (stat(argv[i], &statbuf) < 0)
        {
            perror("stat failed");
            continue;
        }
        if ((fd = open(argv[i], O_RDWR | O_TRUNC)) < 0)
        {
            perror("open failed");
            continue;
        }

        times[0].tv_sec = statbuf.st_atime;
        times[1].tv_sec = statbuf.st_mtime;

        if (futimens(fd, times) < 0)
        {
            perror("futimens failed");
            continue;
        }
        close(fd);
    }
    return 0;
}

测试结果如下:

# 查看文件长度和最后修改时间
blduan@ubuntu:~$ ls -l utimens_test.file 
-rw-rw-r-- 1 blduan blduan 13 Mar 14 08:34 utimens_test.file
# 查看最后访问时间
blduan@ubuntu:~$ ls -lu utimens_test.file 
-rw-rw-r-- 1 blduan blduan 13 Mar 14 08:34 utimens_test.file
# 查看当前时间
blduan@ubuntu:~$ date
Mon 14 Mar 2022 08:35:10 AM PDT
# 执行程序清空文件
blduan@ubuntu:~$ ./utimens_test utimens_test.file 
# 查看清空后的文件长度和最后修改时间,长度变为0,时间未变
blduan@ubuntu:~$ ls -l utimens_test.file 
-rw-rw-r-- 1 blduan blduan 0 Mar 14 08:34 utimens_test.file
# 查看最后访问时间未变
blduan@ubuntu:~$ ls -lu utimens_test.file 
-rw-rw-r-- 1 blduan blduan 0 Mar 14 08:34 utimens_test.file
# 查看状态修改时间发生变化
blduan@ubuntu:~$ ls -lc utimens_test.file 
-rw-rw-r-- 1 blduan blduan 0 Mar 14 08:35 utimens_test.file
blduan@ubuntu:~/Documents/code_snippet/4_chapter$ 

目录

在Linux中目录是一个文件,文件的内容一系列i节点编号和对应的文件名。

创建和删除目录

下面的函数用来创建和删除目录:

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

int mkdir(const char *pathname, mode_t mode);

#include <fcntl.h>           /* Definition of AT_* constants */
#include <sys/stat.h>

int mkdirat(int dirfd, const char *pathname, mode_t mode);

#include <unistd.h>

int rmdir(const char *pathname);
// 成功返回0,失败返回-1

mkdirmkdirat函数会创建空目录文件,文件中的...目录项都是自动创建的。所指定的文件访问权限mode由进程的文件模式创建屏蔽字修改。

对于目录通常最少需要指定一个可执行权限,用来访问目录中的文件名。

目录的所有者ID为进程的有效用户ID,目录的组ID为进程的有效组ID。

fd参数值为AT_FDCWD或pathname为绝对路径时,mkdiratmkdir完全一致。

rmdir函数可以删除空目录,空目录只包含...两项。

如果调用rmdir函数使目录的链接计数成为0,并且也没有其他进程打开此目录,则释放目录占用的空间。

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

int main()
{
    // 屏蔽组执行权限
    umask(S_IXGRP);
    if (mkdir("./mkdir_test1", S_IRWXU | S_IRWXG) < 0)
        perror("mkdir error");
    else
        printf("mkdir ok\n");

    //屏蔽其他用户执行权限
    umask(S_IXOTH);
    if (mkdirat(AT_FDCWD, "./mkdir_test2", S_IRWXU | S_IRWXG | S_IRWXO) < 0)
        perror("mkdirat error");
    else
        printf("mkdirat ok\n");

    sleep(30);
    if (rmdir("./mkdir_test1") < 0)
        perror("rmdir error");
    else
        printf("rmdir ok\n");

    return 0;
}

执行结果如下:

blduan@ubuntu:~/Documents/code_snippet/4_chapter$ ./mkdir_test &
[1] 25442
blduan@ubuntu:~$ mkdir ok
mkdirat ok

blduan@ubuntu:~$ ls -ld mkdir_test1
drwxrw---- 2 blduan blduan 4096 Mar 15 07:42 mkdir_test1
blduan@ubuntu:~$ ls -ld mkdir_test2
drwxrwxrw- 2 blduan blduan 4096 Mar 15 07:42 mkdir_test2
blduan@ubuntu:~$ rmdir ok
blduan@ubuntu:~$ ls -ld mkdir_test1
ls: cannot access 'mkdir_test1': No such file or directory

读目录

进程的工作目录

每个进程都有一个工作目录,该目录是当前进程中所有相对目录的起点。

默认情况下,进程的工作目录由登陆时/etc/passwd文件中用户登陆项的第6个字段决定。如blduan:x:1000:1000:blduan,,,:/home/blduan:/bin/bash

进程可以调用chdirfchdir函数更改当前工作目录:

#include <unistd.h>

int chdir(const char* pathname);
int fchdir(int dirfd);
// 成功返回0, 失败返回-1

当前工作目录是进程的一个属性,仅影响调用chdir或fchdir的进程自身。

由于内核仅为进程维护一个指向其工作目录的i节点的指针,没有工作目录路径,因此需要提供一个函数来获取进程的当前工作目录。

#include <unistd.h>

char* getcwd(char* buf, size_t size);
// 成功返回buf,失败返回NULL

下面验证函数功能:

#include <fcntl.h>
#include <limits.h>
#include <linux/limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main()
{
    char* buf = (char*)malloc(NAME_MAX + 1);
    if (buf != NULL)
    {
        memset(buf, 0x00, NAME_MAX + 1);
        if (getcwd(buf, NAME_MAX + 1))
        {
            printf("current directory is %s\n", buf);
        }

        printf("will change work directory is /tmp\n");
        if (chdir("/tmp") < 0)
        {
            perror("chdir failed");
        }
        else
        {
            printf("chdir ok\n");
        }

        memset(buf, 0x00, NAME_MAX + 1);
        if (getcwd(buf, NAME_MAX + 1))
        {
            printf("current directory is %s\n", buf);
        }

        printf("will change work directory /home/blduan\n");
        int fd = open("/home/blduan", O_RDONLY);
        if (fchdir(fd) < 0)
        {
            perror("fchdir failed");
        }
        else
        {
            printf("fchdir ok\n");
        }

        memset(buf, 0x00, NAME_MAX + 1);
        if (getcwd(buf, NAME_MAX + 1))
        {
            printf("current directory is %s\n", buf);
        }
        free(buf);
    }
}

// 执行结果如下
// current directory is /home/blduan/Documents/projects/code_snippet/4_chapter
// will change work directory is /tmp
// chdir ok
// current directory is /tmp
// will change work directory /home/blduan
// fchdir ok
// current directory is /home/blduan

当程序需要返回其工作起点目录时,可以提前用getcwdopen函数分别保存目录名或文件描述符,之后用chdirfchdir切换目录。

设备特殊文件