UNIX环境高级编程学习之路(二)----文件和目录

本文是关于UNIX环境高级编程的学习笔记,聚焦于文件和目录的处理。介绍了stat、fstat、lstat函数,讲解了umask、chmod、fchmod在权限管理中的应用,探讨了粘住位、空洞文件的概念,并详细阐述了UNIX文件系统的多样性和结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对于UNIX环境编程,工作中经常会用到相关知识,作为学习UNIX环境编程的经典书籍--UNIX环境高级编程,是每个UNIX编程人员必看的经典书籍之一,为了将相关知识重新进行学习,以系统的整合所学知识,遂以博文形式作为总结。

一、概述

本章将描述文件系统的其他特征和文件的性质。将从stat函数开始,诸葛说明stat结构的每一个成员以了解文件的所有属性。在此过程中我们将说明修改这些属性的各个函数(更改所有者、更改权限等),还将更详细的查看UNIX文件系统的结构以及符号链接。


二、stat、fstat和lstat函数

#include<sys/stat.h>
int stat(const char *restrict pathname,struct stat *restrict buf);
int fstat(int filedes, struct stat *buf);
int lstat(const char *restrict pathname,struct stat *restrict buf);
三个函数的返回值:若成功则返回0,若出错则返回-1

    一旦给出pathname,stat函数就返回与此命名文件相关的信息结构。fstat函数获取已在filedes上打开的有关信息。lstat函数类似于stat,但是当命名的文件是一个符号链接时,lstat返回该符号链接的有关信息,而不是由符号链接引用文件的信息。
    第二个参数是buf指针,它指向一个我们必须提供的结构。这些函数填写由buf指向的结构。该结构的实际定义可能随实现有所不同,但其基本形式是:
struct stat {
    mode_t st_mode;     /* file type & mode (permissions) */
    ino_t      st_ino;        /* i-node number (serial number) */
    dev_t     st_dev;       /* device number (file system) */
    dev_t     st_rdev;      /* device number for special files */
    nlink_t    st_nlink;     /* number of links */
    uid_t      st_uid;        /* user ID of owner */
    gid_t      st_gid;        /* group ID of group */
    off_t       st_size;      /* size in bytes, for regular files */
    time_t    st_atime;    /* time of access */
    time_t    st_mtime;   /* time of last modification */
    time_t    st_ctmie;    /* time of last file status change */
    blksize_t st_blksize; /* best I/O block size */
    blkcnt    st_blocks;   /*number of disk blocks allocated */
};
使用stat函数最多的是ls -l命令,用其可以获得有关一个文件的所有信息。

三、文件类型
至今已经介绍了两种不同的文件类型---普通文件和目录。UNIX系统的大多数文件是普通文件或目录,但是也有另外一些文件类型。文件类型包括如下:
(1)普通文件(regular file)这种文件包含了某种形式的数据。至于这种数据是文本还是二进制数据对于UNIX内核而言并无区别。对普通文件内容的解释由处理该文件的应用程序进行。
(2)目录文件(directory file)这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针。对一个目录文件具有读权限的任一进程都可以读该目录的内容,但只有内核可以直接写目录文件。
(3)块特殊文件(block special file)这种文件类型提供对设备(例如磁盘)带缓冲的访问,每次访问以固定长度为单位来进行。
(4)字符特殊文件(character special file)这种文件类型提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件。
(5)FIFO,这种类型文件用于进程间通信,有时也将其称为命名管道(named pipe)
(6)套接字(socket)这种文件类型用于进程间的网络通信。套接字也可以用于在一台宿主机上进程间的非网络通信。
(7)符号链接(symbolic link)这种文件类型指向另一个文件。
    文件类型信息包含在st_mode成员,可以用下表的宏确定文件类型。

POSIX.1允许实现将进程间通信(IPC)对象(例如,消息队列和信号量等)表示为文件。

以下代码为:对每一个命令行参数打印文件类型
#include <stdio.h> //for printf
#include <stdlib.h> //for exit
#include <sys/stat.h> //for S_ISREG()/S_ISDIR() and so on
int main(int argc,char *argv[])
{
    char *ptr;
    struct stat buf;
    int i;
    
    for(i=1; i<argc; i++)
    {
        printf("argv[%d]:%s: ",i,argv[i]);    
        if(lstat(argv[i],&buf) < 0)
        {
            perror("lstat error");
            continue;    
        }
        if(S_ISREG(buf.st_mode))
         ptr = "regular";
        else if(S_ISDIR(buf.st_mode))
         ptr = "directory";
        else if(S_ISCHR(buf.st_mode))
         ptr = "character special";
        else if(S_ISBLK(buf.st_mode))
         ptr = "block special";
        else if(S_ISFIFO(buf.st_mode))
         ptr = "fifo";
        else if(S_ISLNK(buf.st_mode))
         ptr = "symbolic link";
        else if(S_ISSOCK(buf.st_mode))
         ptr = "socket";
        else
            ptr = "***unknown mode***";
        
        printf("%s\n",ptr);
    }
    exit(0);
}
程序事例输出:./a.out /etc/passwd /etc /dev/initctl /dev/log /dev/tty /dev/scsi/host0/bus0/target0/lun0/cd /dev/cdrom
/etc/passwd:regular
/etc:directory
/dev/initctl:fifo
/dev/log:socket
/dev/tty:character special
/dev/scsi/host0/bus0/target0/lun0/cd:block special
/dev/cdrom:symbolic link

四、 设置用户ID和设置组ID
与一个进程相关的ID有6个或者更多,如下表:


* 实际用户ID和实际组ID标识我们究竟是谁。这两个字段在登录时取自口令文件中的登陆项。通常,在一个登录会话间这些值并不改变,但是超级用户进程有方法改变它们;
* 有效用户ID,有效阻ID以及附加组ID决定了我们的文件访问权限;
* 保存的设置用户ID和保存的设置组ID在执行一个程序时包含了有效用户ID和有效阻ID的副本。
    通常,有效用户ID等于实际用户ID,有效组ID等于实际组ID。
每个文件都有一个所有者和组所有者,所有者和组所有者分别用st_uid成员和st_gid成员来表示。
      当执行一个程序文件时,进程的有效用户ID通常就是实际用户ID,有效组ID通常就是实际组ID。但是可以在文件模式字(st_mode)中设置一个特殊标志,其含义是“当执行此文件时,将进程的有效用户ID设置为文件所有者的用户ID(st_uid)”。与此相类似,在文件模式字中可以设置另外一位,它使得将执行此文件的进程的有效组ID设置为文件的组所有者ID(st_gid)。在文件模式字中的这两位被称为设置用户ID(set-user-ID)位和设置组ID(set-group-ID)位。
    例如,若文件所有者是超级用户,并且设置了该文件的设置用户ID位,然后当该程序由一个进程执行时,则改进程就有超级用户特权。不管执行此文件的进程的实际用户ID是什么,都进行这种处理。
关于实际用户ID,有效用户ID,文件所有者,有如下通俗的解释:
实际用户ID: 有的文章中将其称为真实用户ID, 这个ID就是我们登陆unix系统时的身份ID。
有效用户ID: 定义了操作者的权限。有效用户ID是进程的属性,决定了该进程对文件的访问权限。
文件的访问权限包括读写和执行 。判断某个进程对文件有何权限时,内核会将非超级用户进程的有效ID与文件的所有者ID进行比较 ,当然,也可能需要比较有效组ID,这关系到具体的权限测试方法,先不在这里说明。 而超级用户创建的进程是允许访问整个文件系统的。它的有效ID等于0。不过,这里还有一点需要说明的是,仅仅有合适的有效ID,还不一定就能获得所有或者部分权限。你需要得到被访问文件的允许,这就是文件访问权限位(用户读、用户写、组读等)的责任了。
文件的所有者ID创建文件是由某用户的进程实现的吧? 所以在创建新文件的时候,就将该进程的有效ID作为该文件的所有者ID了。 APUE里面有时又将文件的所有者ID称为“文件的用户ID”。也就是该文件是由哪个用户创建的,可用通过ls -l来查看到。

五、文件访问权限
st_mode值也包括了针对文件的访问权限位,当提及文件时,指的是前面提到的任何类型的文件。所有文件类型(目录、字符特别文件等)都有访问权限。

* 我们用名字打开任一类型的文件时,对该名字中包含的每一个目录,包括它可能隐藏的当前工作目录都应具有执行权限。所以,对应目录的执行权位常被称为搜索位。
    例如,打开文件/usr/include/stdio.h,需要对/、/usr和/usr/include具有执行权限。然后,需要具有对该文件本身的适当权限,这取决于以何种模式打开它(只读、读写等)。
    如果当前目录是/usr/include,那么为了打开文件stdio.h,需要对该工作目录的执行权限。这是隐含当前工作目录的一个实例,打开stdio.h文件与打开./stdio.h作用相同。
    注意,对于目录的读权限和执行权限是不同的。读权限允许我们读目录,获得在该目录中所有文件的列表。当一个目录是我们要访问的路径名的衣蛾组成部分时,对该目录的执行权限使我们可以通过该目录。
* 为了在open函数中对一个文件制定O_TRUNC标志,必须对该文件有写权限。
* 为了在一个目录中创建一个新文件,必须对该目录具有写权限和执行权限。
* 为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限。对该文件本身则不需要有读、写权限。
如果用6个exec函数中的任何一个执行某个文件,都必须对该文件具有执行权限。该文件还必须是一个普通文件。

六、新文件和目录的所有权
新文件的用户ID设置为进程的有效用户ID。关于组ID,POSIX.1允许实现选择下列之一作为新文件的组ID。
(1)新文件的组ID可以是进程的有效组ID。
(2)新文件的组ID可以是他所在的目录的组ID。
    使用POSIX.1所允许的第二个选项(继承目录的组ID)使得在某个目录下创建的文件和目录都具有该目录的组ID。于是文件和目录的组所有权从该点向下传递

七、access函数
当用open函数打开一个文件时,内核以进程的有效用户ID和有效组ID为基础执行其访问权限测试。有时,进程也希望按其实际用户ID和实际组ID来测试其访问能力。access函数是按实际用户ID和实际组ID进行访问权限测试的。
#include<unistd.h>
int access(const char *pathname, int mode);
返回值:若成功则返回0,瑞出错则返回-1;
其中,mode是下表所列常量的按位或。

如下测试程序显示了access函数的使用方法。
#include <stdio.h> //for printf
#include <stdlib.h> //for exit
#include <unistd.h> //for access
#include <fcntl.h> //for open
int main(int argc,char *argv[])
{
    if(argc != 2)
    {
        printf("usage:./a.out ");
        exit(1);    
    }
    if(access(argv[1],R_OK) < 0)
    {
        printf("access error for %s\n",argv[1]);
        exit(1);    
    }
    else
    {
        printf("read access OK\n");    
    }
    if(open(argv[1],O_RDONLY) < 0)
    {
        printf("open error for %s\n",argv[1]);
        exit(1);    
    }
    else
    {
        printf("open for reading OK\n");    
    }
    exit(0);
}
设置用户ID程序可以确定实际用户不能读某个指定文件,而open函数却能打开该文件。


八、umask函数

umask函数为进程设置文件模式创建屏蔽字,并返回以前的值。

#include<sys/stat.h>
mode_t umask(mode_t cmask);
返回值:以前的文件模式创建屏蔽字,该函数是没有出错返回的函数。

其中,参数cmask是9个访问权限位常量(S_IRUSR、S_IWUSR)中的若干个按位“或”构成的。在进程创建一个
新文件或者新目录时,就一定会使用文件模式创建屏蔽字。对弈任何在文件模式创建屏蔽字中为1 的位,在文件mode中的相应位则一定关闭。
如下程序创建了两个文件,创建第一个时,umask值为0,创建第二个时,umask值禁止所有组或其他用户的访问权限。
#include "apue.h"
#include<fcntl.h>
#define RWRWRW (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IROTH)
int main(void)
{
        mode_t mask=umask(0);
        if(creat("foo",RWRWRW)<0)
           err_sys("create error for foo");
        umask(S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
        if(creat("bar",RWRWRW)<0)
           err_sys("creat error for bar");
        exit(0);
}
执行结构为:

UNIX系统的大多数用户从不处理他们的umask值。通常在登录时,由shell的启动文件设置一次,然后从不改变。尽管如此,当编写创建新文件的程序时,如果我们想确保指定的访问权限位已经激活,那么必须在进程运行时修改umask值。例如,如果我们想确保任何用户都能读文件,则应将umask设置为0。否则,当我们的进程运行时,有效的umask值可能关闭该权限位。
更改进程的文件模式创建屏蔽字并不影响其父进程(通常是shell)的屏蔽字。所有shell都有内置的umask命令,我们可以用命令设置或打印当前文件模式创建屏蔽字。
    用户可以设置umask值以控制他们所创文件的默认权限。该值表示成八进制数,一位代表一种要屏蔽的权限,设置了相应位后,塔索对应的权限就会被拒绝。如:
002:阻止其他用户写你的文件
022:阻止同组成员和其他用户写你的文件
027:阻止同组成员写你的文件以及其他用户读、写或者执行你的文件。

Single UNIX Specification要求shell支持符号形式的umask命令。与八进制格式不同,符号格式指定许可的权限而非拒绝的权限。下面比较了两种格式的命令。


九、chmod、fchmod函数

这两个函数使我们可以更改现有文件的访问权限

#include<sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int filedes, mode_t mode);
返回值:若成功则返回0,若出错则返回-1。

为了改变一个文件的权限位,进程的有效用户ID必须等于文件的所有者ID,或者该进程必须具有超级用户权限。 
    参数mode是如下变所示常量的某种按位或运算构成的。


如下程序修改foo文件和bar文件模式

#include "apue.h"
int 
main(void)
{
        struct stat     statbuf;
        /* turn on set-group-ID and turn off group-execute */
        if(stat("foo", &statbuf) < 0)
                err_sys("stat error for foo");
        if(chmod("foo", (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0)
                err_sys("chmod error for foo");
        /* set absolute mode to "rw-r--r--" */
        if(chmod("bar", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) < 0)
                err_sys("chmod error for bar");
        exit(0);
}

statbuf.st_mode & ~S_IXGRP 是 把组执行权限设置为0,其它权限不变。
程序运行后,这两个文件的最终状态为:

chmod函数更新的只是i节点最近一次被更改的时间,系统默认,ls -l列出的是最后文件内容的时间。

十、粘住位

S_ISVTX位被称为粘住位(sticky bit)。如果一个可执行程序文件的这一位被设置了,那么在该程序第一次被执行并结束时,其程序正文部分的一个副本仍被保存在交换区。这使得下次执行该程序时能较快的将其装入内存区。
    如今的系统扩展了粘住位的使用范围,Single UNIX Specification允许针对目录设置粘住位。如果对一个目录设置了粘住位,怎只有对该目录具有写权限的用户在满足下列条件之一的情况下,才能删除该目录下的文件:
 * 拥有此文件
* 拥有此目录
 *是超级用户
目录/tmp和/var/spool/uucppublic是设置粘住位的典型候选者——任何用户都可在这两个目录中创建文件。任一用户(用户、组和其他)对这两个目录的权限通常都是读、写和执行。但是用户不应能删除或更名属于其他人的文件,为此在这两个目录的文件模式中都设置了粘住位。

十一、 chown、fchown和lchown函数

下面几个chown函数可用于更改问价 的用户ID和组ID

#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int filedes, uid_t owner, gid_t group);
int lchown(const char *pathname , uid_t owner, gid_t group);
三个函数的返回值:若成功则返回0,若出错则返回-1

除了所引用的文件时符号链接以外,这三个函数的操作相似。在符号链接的情况下,lchown更改符号链接本身的所有者,而不是符号链接所指向的文件。
    如若两个参数owner过group中的任意一个是-1,则对应的ID不变。
若_POSIX_CHOWN_RESTRICTED对指定的文件起作用,则
(1)只有超级用户进程能更改该文件的用户ID。
(2)若满足下列条件,一个非超级用户进程就可以更改该文件的组ID:
        (a)进程拥有此文件(其有效用户ID等于该文件的用户ID)。
        (b)参数owner等于-1或文件的用户ID,并且参数group等于进程的有效组ID或进程的附属组ID之一。
    这意味着,当 _POSIX_CHOWN_RESTRICTED其作用时,不能更改其他用户文件的用户ID。你可以更改你所拥有的文件的组ID,但只能改到你所属的组。
    如果这些函数由非超级用户进程调用,则在成功返回时,该文件的设置用户ID位和设置组ID位都会被清除。


十二、文件长度

stat结构成员st_size表示以字节为单位的文件长度,此字段只对普通文件、目录文件和符号链接有意义。
对于普通文件,其文件长度可以是0,在读这种文件时,将得到文件结束(end-of-file)指示。
对于目录,文件长度通常是一个数(例如16或者512)的倍数。
对于符号链接,文件长度是文件名中实际字节数。例如,

其中,文件长度7就是路径名usr/lib的长度。
如今,大多数UNIX系统提供字段st_blksize和st_blocks。其中,第一个是对文件I/O较为合适的块长度,第二个时所分配的实际512字节块数量。
    文件中的空洞

空洞是由所设置的偏移量超过文件尾端,并写了某些数据后造成的。对于没有写过的字节位置,read函数读到的字节是0.

如果使用实用程序(例如cat(1))复制这种文件,那么所有这些空洞都会被填满,其中所有实际数据字节皆填写为0.


十三、文件截短

有时我们需要在文件尾端截去一些数据为了缩短文件。将一个文件清空为0是一个特例,在打开文件时使用O_TRUNC标志可以做到这一点。

#include <unistd.h>

int truncate(const char* pathname, off_t length);

int ftruncate(int filedes, off_t length);

返回值:若成功返回0,失败则返回-1
这两个函数将把现有的文件长度截短为length字节。如果该文件以前的长度大于length,则超过length以外的数据将不能再访问。如果以前的长度短于length,则其效果与系统有关。若UNIX系统实现扩展了该文件,则在以前的文件尾端和新的文件尾端之间的数据将读作0(也就是可能在文件中创建了一个空洞)。


十四、文件系统

目前,正在使用的UNIX文件系统有多种实现。例如,Solaris支持多种不同类型的磁盘文件系统:传统的基于BSD的UNIX文件系统(UFS,UNIX file system),读、写DOS格式化软盘的文件系统(称为PCFS),以及读CD的文件系统(HSFS)。

我们可以把一个磁盘分成一个或多个分区。每个分区可以包含一个文件系统。(如下图:


 i节点是固定长度的记录项,它包含有关文件的大部分信息。
如果更仔细的观察一个柱面组的i节点和数据块部分,则可以看到 如下图所示的情况:

如上图,注意下面个点:
*在图中有两个目录项指向同一个i节点。每个i节点中都有一个链接计数,其值是指向该i节点的目录项数。只有当链接计数减少至0时,才可删除该文件(也就是可以释放该文件占用的数据块)。这就是为什么“删除对一个文件的链接”操作并不总是意味着“释放该文件占用的磁盘块”的原因。在stat结构中,链接计数包含在st_nlink成员中,其基本系统数据类型是nlink_t。这种链接类型称为硬链接,LINK_MAX指定了一个文件连接数的最大值。
*另外一种链接类型称为符号链接。对于这种链接,该文件的实际内容(在数据块中)包含了该符号链接所指向的文件的名字。在下例子中:

该目录项中的文件名是3字符的字符串lib,而在该文件中包含了7个数据字节usr/lib。gaii节点中的文件类型是S_IFLNK,于是系统知道这是一个符号链接。
*i节点包含了大多数与文件有关的信息:文件类型、文件访问权限位、文件长度和指向该文件所占用的数据块的指针等。 stat结构的大多数信息都是取自i节点。只有两项数据存放在目录项中:文件名和i节点编号。i节点编号的数据类型是ino_t。
*每个文件系统各自对他们的i节点进行编号,因此目录项中的i节点编号数指向同一文件系统中的相应i节点,不能使一个目录项指向另一个文件系统的i节点。
*当在不更换文件系统情况下为一个文件更名时,该文件的实际内容并未移动,只需构造一个指向现有i节点的心目录项,并解除与旧目录项的链接。这就是mv(1)命令的通常操作方式。

十五、 link、unlink、remove和rename函数
任何一个文件可以有多个目录项指向其i节点,创建一个现有文件的链接的方法是使用link函数。
#include<unistd.h>
int link(const char *existingpath, const char *newpath);
返回值:若成功返回0,若出错返回-1
此函数创建一个新目录项newpath,它引用现有的文件existingpath。若果newpath已经存在,则返回出错。
创建新目录项以及增加链接计数应当是个原子操作,大多数实现要求这两个路径名在同一个文件系统中。如果实现支持创建执行一个目录的硬链接,那么也是仅限于超级用户才可以这么做。
为了删除一个现有的目录项,可以调用unlink函数。
#include <unistd.h>
int unlink(const char *pathname);
返回值:若成功返回0,若出错返回-1
此函数删除目录项,并将由pathname所引用文件的链接计数减1。如果还有指向该文件的其他链接,则仍然可以通过其他链接访问该文件的数据。如果出错,怎不对该文件做任何更改。
为了解除对文件的链接,必须对包含该目录项目录具有写和执行权限。如果对该目录设置了粘着位,则对该目录必须具有写权限,并且具备下面三个条件之一:
1.拥有该文件
2.拥有该目录
3.具有超级用户特权
只有链接计数达到0时,该文件的内容才可以被删除。只要有进程打开了该文件,其内容也不能删除。关闭一个文件时,内核首先检查打开该文件的进程数。如果该数达到0,然后内核检查其连接数,如果这个数也是0,那么就删除该文件的内容。
下例为程序打开一个文件,然后接出对他的锁定。
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
        if (open("tmpfile",O_RDWR) < 0)
        {
                printf("open error\n");
        }
        if (unlink("tmpfile") < 0)
        {
                printf("unlink error\n");
        }
        sleep(15);
        printf("done\n");
        return 0;
}
运行该程序,其结果是

unlink的这种性质经常被程序用来确保及时是在该程序奔溃时,他所创建的临时文件也不会遗留下来。进程用open或create创建一个文件,然后立即调用unlink。因为该文件仍旧是打开的,所以不会将其内容删除。只有当进程关闭该文件或终止时(在这种情况下,内核会关闭该进程打开的全部文件),该文件的内容才会被删除。
    如果pathname是符号链接,那么unlink删除该符号链接,而不会删除由该链接所引起的文件。给出符号链接名情况下,没有一个函数能删除该链接所引用的文件。
    我们也可以用remove函数接出一个文件或者目录的链接。对于文件,remove的功能与unlink相同。对于目录,remove的功能与rmdir相同。
#include <stdio.h>
int remove(const char* pathname);
返回值:若成功则返回0,若出错则返回-1;
文件或者目录用rename函数更名。
#include<stdio.h>
int rename(const char *oldname, const char *newname);
返回值:若成功则返回0,若出错则返回-1;
根据oldname是指文件还是目录,有几种情况要说明:
(1)、如果oldname指的是一个文件而不是目录,那么为该文件或者符号链接更名。在这种情况下,如果newname已经存在,则他不能引用一个目录。如果newname已经存在,而且不是一个目录,则现将该目录项删除然后将oldname更名为newname。对包含oldname的目录以及包含newname的目录,调用进程必须具有写权限,因为将更改这两个目录。
(2)、如若oldname指的是一个目录,那么为该目录更名。如果newname已经存在,则他必须引用一个目录一,而且该目录应当是空目录(空目录指的是该目录中只有 . 和.. 项)。如果newname存 在(而且是一个空目录),则先将其删除,然后将 oldname更名为newname。另外,当为一个目 录更名时, newname不能包含oldname作为其路径前缀。例如,不能将/usr/foo 更名为 /usr/foo/testdir,因为老名字(/usr/foo)是新名字的路径前缀,因而不能将其删除。
(3)、如若oldname或newname引用符号链接,则处理的是符号链接本身,而不是他所引用的文件。
(4)、作为一个特例,如果oldname和newname引用同一个文件,则函数不做任何更改而成功返回。

十六、符号链接
    符号链接是指向一个文件的间接指针,它与硬链接有所不同,硬链接直接指向文件的i节点。引入符号链接的原因是为了避开硬链接的一些限制:
*硬链接通常要求链接和文件位于同一文件系统中。
*只有超级用户才能创建指向目录的硬链接。
    对符号链接以及他指向各种对象并无任何文件系统限制,任何用户都可以创建指向目录的符号链接。符号链接一般用于将一个文件或整个目录结构移到系统中的另一个位置。
    当使用以名字引用文件的函数时,应当了解该函数是否处理符号链接。也就是该函数是否跟随符号链接到达他所连接的文件。如果该函数具有处理符号链接的功能,则其路径名参数引用由符号链接所指向的文件。否则,路径名参数将引用链接本身,而不是该链接指向的文件。    
下表列出了本章所说明的各个函数是否处理符号链接,表中没有列出mkdrir、mkinfo、mknod、rmdir这些函数,其原因是,当路径名是符号链接时,他们都出错返回。以文件描述符作为参数的一些函数(如fstat、fchmod等)也未在该表中列出,其原因是,对于符号链接的处理是由返回文件描述符的函数(通常是open)进行的。chown是否跟随符号链接取决于实现。


引入符号链接可能在文件系统中引入循环。大多数查找路径名的函数在这种情况发生时都将返回值为ELOOP的errno。考虑下列命令序列:

这里创建了一个目录foo,它包含了一个名为a的文件以及一个指向foo的符号链接。在下图显示了这种这种结果,以圆表示目录,正方形表示一个文件。如果我们编写一段程序,使用Solaris的标准函数ftw(3)以降序遍历文件结构,打印每个遇到的路径名:其结果输出是:

这样一个循环式很容易消除的。因为unlink并不跟随符号链接,所以可以unlink文件foo/testdir。但是如果创建了一个构成循环的硬链接,那么就很难消除它。这就是为什么link函数不允许构造指向目录的硬链接的原因(除非进程具有超级用户特权)。
    当open打开文件时,如果传递给open函数的路径名指定了一个符号链接,那么open跟随此链接到达你所指定的文件。若此符号链接所指向的文件并不存在,则open返回出错,表示他不能打开该文件。


十七、symlink和readlink函数
symlink函数创建一个符号链接。
#include <unistd.h>
int symlink(const char *actualpath, const char *sympath);
返回值:若成功怎返回0,若出错则返回-1;
在创建此符号链接时,并不要求actualpath已经存在。并且,actualpath和sympath并不需要位于同一文件系统中。
因为open函数跟随符号链接,所以需要有一种方法打开链接本身,并读该链接中的名字。readlink函数提供了这种功能。
#include<unistd.h>
ssize_t readlink(const char* restrict pathname, char *restrict buf,size_t bufsize);
返回值:若成功则返回读到的字节数,若出错则返回-1;
此函数结合了open、read和close的所有操作。如果此函数成功执行,则他返回读入buf的字节数。在buf中返回的符号链接的内容不以null字符终止。

十八、文件的时间
对于每个文件保持有三个时间字段,如下表:
注意修改时间(st_mtime)和更改状态时间呢(st_ctime)之间的区别。修改时间是文件内容最后一次被修改的时间。更改时间状态是该文件的i节点最后一次被修改的时间。有很多操作,他们影响到i节点,但没有更改文件的实际内容:文件的存取许可权、用户ID、连接数等等。因为i节点中的所有信息都是与文件的实际内容分开存放的,所以,除了文件夹数据修改时间以外,还需要更改状态时间。
    注意,系统并不保存对一个i节点的最后一次存取时间,所以access和stat函数并不更改这三个时间中的任意一个。
   系统管理员常常使用存取时间来删除在一定时间范围内没有存取过的文件。典型的例子是删除在过去一周内没有存取过的名为a.out或core的文件。find(1)命令常被用来进行这种操作。
    修改时间和更改时间状态可被用来归档其内容已经被修改或者其i节点已经被更改的那些文件。
    ls命令按这三个时间值中的一个进行排序显示。按系统默认,他按文件的修改时间的先后排序显示。-u选择项使其用存取时间排序,-c选择项则使其用刚改状态时间排序。



十九、 函数futimes、utimenstat和utimes
一个文件的访问和修改时间可以用以下几个函数修改。futimens和utimensat函数可以指定纳秒级精度的时间戳。用到的数据结构是与stat函数族相同的timespec结构。
#include <sys/stat.h>
int futimes(int fd, const timespec times[2]);
int utimensat(int fd, const char *path, const struct timespec times[2], int flag);
返回值:若成功返回0,若出错返回-1
这两个函数的times数组参数的第一个元素包含访问时间,第二个元素包含修改时间。这两个时间值是日历时间。
时间戳可以按下列4种方式之一进行指定。
struct timespec
{
    __time_t tv_sec;        /* Seconds. */
    long int tv_nsec;       /* Nanoseconds. */
};

(1)如果times参数是一个空指针,则访问时间和修改时间两者都设置为当前时间。
(2)如果times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_NOW,相应的时间戳设置为当前时间,忽略相应的tv_sec字段。
(3) 如果times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_OMIT,在这种时间戳保持不变,忽略相应的tv_sec字段。
(4) 如果times参数指向两个timespec结构的数组,且tv_nsec字段的值为既不是UTIME_NOW也不是UTIME_OMIT,在这种情况下,相应的时间戳设置为相应的tv_sec和tv_nsec字段的值。
执行这些函数所要求的优先级取决于times参数的值。
* 如果times是一个空指针,或者任一tv_nsec字段设为UTIME_NOW,则进程的有效用户ID必须等于该文件的所有者ID;进程对该文件必须具有写权限,或者进程是一个超级用户进程。
* 如果times是非空指针,并且任一tv_nsec字段的值既不是UTIME_NOW也不是UTIME_OMIT,则进程的有效用户ID必须等于该文件的所有者ID,或者进程必须是一个超级用户进程。对文件只具有写权限是不够的。
* 如果times是非空指针,并且两个tv_nsec字段的值都为UTIME_OMIT,就不执行任何的权限检查。
    futimens函数需要打开文件来更改他的时间,utimenatat函数提供了一个使用文件名更改文件时间的方法。pathname参数是相对于fd参数进行计算的,fd要么是打开目录的文件描述符,要么设置为特殊值AT_FDCWD(强制通过相对于调用进程的当前目录计算pathname)。如果pathname指定了绝对路径,那么fd参数被忽略。
    utimensat的flag参数可用于进一步修改默认行为。如果设置了AT_SSYMLINK_NOLOLLOW标志,则符号链接本身的时间就会被修改。默认的行为是跟随符号链接,并把文件的时间改成符号链接的时间。

#include <sys/time.h>
int utimes(const char *pathname, const struct timeval times[2]);
函数返回值:若成功,返回0,若出错返回-1;
utimes函数对路径名进行操作。times参数是指向包含两个时间戳(访问时间和修改时间)元素组的指针,两个时间戳是用秒和微秒表示的。
struct timeval {
        time_t tv_sec;  /*seconds*/
        long tv_usec;   /*microseconds*/
};
注意,我们不能对状态更改时间st_ctime(i节点最近被修改的时间)指定一个值,因为调用utimes函数时,此字段会被自动更新。
在某些UNIX版本中,touch(1)命令使用这些函数中的某一个。另外,标准归档程序tar(1)和cpio(1)可选的调用这些函数,以便将一个文件的时间值设置为他归档时保存的时间。
如下程序使用自带O_TRUNC选项的open函数将文件长度截短为0,但并不修改其访问时间以及修改时间。
#include <fcntl.h>
#include <stdio.h>
#include <utime.h>
#include <sys/stat.h>
int main(int argc, char **argv)
{
        int i,fd;
        struct stat statbuf;
        struct timespec times[2];
        for (i = 1; i < argc; i++)
        {
                if (stat(argv[i], &statbuf) < 0) {                  /*得到当前时间*/
                        printf("%s:stat error", argv[i]);
                        continue;
                }
                if (fd = open(argv[i], O_RDWR | O_TRUNC) < 0) {     /*截断处理*/
                        printf("%s:open error",argv[i]);
                        continue;
                }
                times[0] = statbuf.st_atim;
                times[1] = statbuf.st_mtim;
                if (futimens(fd, times) < 0) {                      /*重置时间*/
                        printf("%s: futimens error",argv[i]);
                }
                close(fd);
  }
  exit(0);
}

$ ls -1 changemod times      察看长度和最后修改时间
-rwxrwxr-x  1 stevens    24576 Dec     4 16:13 changemod
-rwxrwxr-x  1 stevens    24576 Dec     6 09:24 times
$ ls -lu changemod times     察看最后存取时间
-rwxrwxr-x  1 stevens    24576 Feb     1 12:44 changemod
-rwxrwxr-x  1 stevens    24576 Feb     1 12:44 times
$ date                       打印当天日期
Sun Feb  3 18:22:33 MST 1991
$ a.out changemod times      运行程序
$ ls -1 changemod times      检查结果
-rwxrwxr-x  1 stevens        0 Dec     4 16:13 changemod
-rwxrwxr-x  1 stevens        0 Dec     6 09:24 times
$ ls -lu changemod times     检查最后存取时间
-rwxrwxr-x  1 stevens        0 Feb     1 12:44 changemod
-rwxrwxr-x  1 stevens        0 Feb     1 12:44 times
$ ls -lc changemod times     更改状态时间
-rwxrwxr-x  1 stevens        0 Feb     3 18:23 changemod
-rwxrwxr-x  1 stevens        0 Feb     3 18:23 times

如我们所预见的一样,最后修改时间和最后访问时间未变。但是,状态更改时间则更改为程序运行时的时间。

二十、 函数mkdir、mkdirat和rmdir
用mkdir和mkdirat函数创建目录,用rmdir函数删除目录。
#include<sys/stat.h>
int mkdir(const char *pathname, mode_t mode);
int mkdirat(int fd, const char *pathname, mode_t mode);
返回值:如成功返回0,失败返回-1;
这两个函数创建一个新的空目录。其中,.和..目录项是自动创建的。所指定的文件访问权限由进程的文件创建模式创建屏蔽字修改。
    常见的错误是指定与文件相同的mode(只指定读、写权限)。但是,对于目录通常至少要设置一个执行权限位,以允许访问该目录中的文件名。
mkdirat函数和mkdir函数相似。当fd参数具有特殊值AT_FDCWD或者pathname参数指定了绝对路径名时,mkdirat和mkdirat完全一样。否则,fd参数是一个打开目录,相对路径名根据此打开目录进行计算。
    用rmdir函数可以删除一个空目录。空目录是只包含.和..这两项的目录。
#include<unistd.h>
int rmdir(const char *pathname);
返回值:若成功,返回0,如出错,返回-1;
 如果调用此函数使目录的链接计数称为0,并且也没有其他进程打开此目录,则释放由此目录占用的空间。如果在链接计数到达0时,有一个或者多个进程打开此目录,则在此函数返回前删除最后一个链接以及.和..项。另外,在此目录中不能在创建新文件。但是在最后一个进程关闭它之前并不释放次目录。(即使另一些进程打开该目录,他们在此目录下也不能执行其他操作。这样处理的原因是,为了使rmdir函数成功执行,该目录必须是空的。)

二一、读目录
对某个目录具有访问权限的任一用户都可以读该目录,但是为了防止文件系统产生混乱,只有内核才可以写目录。一个目录的写权限位和执行权限位决定了该目录中能否创建新文件以及删除文件,他们并不表示能否写目录本身。
目录的实际格式依赖于UNIX系统实现和文件系统的设计。很多实现阻止应用程序使用read函数读取目录的内容,由此进一步将应用程序与目录格式中相关的细节隔离。
#include<dirent.h>
DIR *opendir(const char *pathname);
DIR *fdopendir(int fd);
两个函数返回值:若成功返回指针,若出错返回NULL
struct dirent *readdir(DIR *dp);
返回值:若成功,返回指针;若在目录尾或者出错,返回NULL
void rewinddir(DIR *dp);
int closedir(DIR *dp);
 返回值:若成功,返回0,若出错,返回-1
long telldir(DIR *dp);
返回值:与dp关联的目录中的当前位置
void seekdir(DIR *dp, long loc);
fdopendir函数可以把打开文件描述符转换成目录处理函数需要的DIR结构。
定义在头文件<dirent.h>中的dirent结构与实现相关。实现对此结构所做的定义至少包含下列两个成员:
ino_t d_ino;             /*i节点编号*/
char d_name[];       /*以null结束的文件名 * /
    注意,d_name项的大小并没有指定,但必须保证他能包含至少NAME_MAX个字节(不包含终止null字节)。因为文件名是以null字节结束的,所以在头文件中如何定义数组d_name并无多大关系。数值大小并不表示文件名的长度。
    DIR结构是一个内部结构,
上述的函数用这个内部结构保存当前正在被读的目录的有关信息。其作用类似于FILE结构。
 由opendir和fdopendir返回的指向DIR结构的指针由另外5个函数使用。opendir执行初始化操作,使第一个readdir返回目录中的第一个目录项。DIR结构由fdopendir创建时,readdir返回的第一项取决于传给fdopendir函数的文件描述符相关联的文件偏移量。注意,目录中各目录项的顺序与实现有关。他么通常不按字母顺序排列。

下例子为一个遍历文件层次结构的程序
#include "apue.h"
#include "pathalloc.h"
#include <dirent.h>
#include <limits.h>
// function type that is called for each filename
typedef int Myfunc(const char *, const struct stat *, int);
static Myfunc myfunc;
static int myftw(char *, Myfunc *);
static int dopath(Myfunc *);
static long nreg, ndir, nblk, nchr, nfifo, nslink, nsock, ntot;
int main(int argc, char *argv[])
{
    int ret;
    if (argc != 2) {
        err_quit("usage: ftw <starting-pathname>");
    }
    ret = myftw(argv[1], myfunc); // does it all
    ntot = nreg + ndir + nblk + nchr + nfifo + nslink + nsock;
    if (ntot == 0) {
        ntot = 1; // avoid divide by 0; print 0 for all counts
    }
    printf("regular files  = %7ld, %5.2f %%\n", nreg, nreg * 100.0 / ntot);
    printf("directories    = %7ld, %5.2f %%\n", ndir, ndir * 100.0 / ntot);
    printf("block special  = %7ld, %5.2f %%\n", nblk, nblk * 100.0 / ntot);
    printf("char special   = %7ld, %5.2f %%\n", nchr, nchr * 100.0 / ntot);
    printf("FIFLs          = %7ld, %5.2f %%\n", nfifo, nfifo * 100.0 / ntot);
    printf("symbolic links = %7ld, %5.2f %%\n", nslink, nslink * 100.0 / ntot);
    printf("sockets        = %7ld, %5.2f %%\n", nsock, nsock * 100.0 / ntot);
    exit(ret);
}
/*
 * Descend through the hierarchy starting at "pathname".
 * The caller's func() is called for every file.
 * */
#define FTW_F 1    // file other than directory
#define FTW_D 2    // directory
#define FTW_DNR 3  // directory thar can't be read
#define FTW_NS 4   // file that we can't stat
static char *fullpath; // contains full pathname for every file
static size_t pathlen;
static int myftw(char *pathname, Myfunc *func) // we return whatever func() returns
{
    fullpath = path_alloc(&pathlen); // malloc PATH_MAX+1 bytes
    if (pathlen <= strlen(pathname)) {
        pathlen = strlen(pathname) * 2;
        if ((fullpath = realloc(fullpath, pathlen)) == NULL) {
            err_sys("realloc failed");
        }
    }
    strcpy(fullpath, pathname);
    return (dopath(func));
}
/*
 * Descend through the hierarchy, starting at "fullpath".
 * If "fullpath" is anything other than a directory, we lstat() it
 * call func(), and return. For a directory, we call ourself
 * recursively for each name in the directory.
 * */
static int dopath(Myfunc *func) // we return whatever func() returns
{
    struct stat statbuf;
    struct dirent *dirp;
    DIR * dp;
    int ret, n;
    if (lstat(fullpath, &statbuf) < 0) { // stat error
        return (func(fullpath, &statbuf, FTW_NS));
    }
    if (S_ISDIR(statbuf.st_mode) == 0) { // not a directory
        return (func(fullpath, &statbuf, FTW_F));
    }
    /*
     * It's a directory. First call func() for the directory,
     * then process each filename in the directory.
     * */
    if ((ret = func(fullpath, &statbuf, FTW_D)) != 0) {
        return ret;
    }
    n = strlen(fullpath);
    if (n + NAME_MAX +2 > pathlen) { // expand path buffer
        pathlen *= 2;
        if ((fullpath = realloc(fullpath, pathlen)) == NULL) {
            err_sys("realloc failed");
        }
    }
    fullpath[n++] = '/';
    fullpath[n] = 0;
    if ((dp = opendir(fullpath)) == NULL) { // can't read directory
        return func(fullpath, &statbuf, FTW_DNR);
    }
    while ((dirp = readdir(dp)) != NULL) {
        if (strcmp(dirp->d_name, ".") == 0 ||
            strcmp(dirp->d_name, "..") == 0) {
            continue; // ignore dot and dot-dot
        }
        strcpy(&fullpath[n], dirp->d_name); // append name agter "/"
        if ((ret = dopath(func)) != 0) { // recursive
            break; // time to leave
        }
    }
    fullpath[n - 1] = 0; // erase everything from slash onward
    if (closedir(dp) < 0) {
        err_ret("can't close directory %s", fullpath);
    }
    return ret;
}
static int myfunc(const char *pathname, const struct stat *statptr, int type)
{
    switch (type) {
    case FTW_F:
        switch (statptr->st_mode & S_IFMT) {
        case S_IFREG:
            ++nreg;
            break;
        case S_IFBLK:
            ++nblk;
            break;
        case S_IFCHR:
            ++nchr;
            break;
        case S_IFIFO:
            ++nfifo;
            break;
        case S_IFLNK:
            ++nslink;
            break;
        case S_IFSOCK:
            ++nsock;
            break;
        case S_IFDIR: // directories should have type = FTW_D
            err_dump("for S_IFDIR for %s", pathname);
        }
        break;
    case FTW_D:
        ++ndir;
        break;
    case FTW_DNR:
        err_ret("can't read directory %s", pathname);
        break;
    case FTW_NS:
        err_ret("stat error for %s", pathname);
        break;
    default:
        err_dump("unknow type %d for pathname %s", type, pathname);
    }
    return 0;
}

二二、 函数chdir、fchdir和getcwd
每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点(不以斜线开始的路径名为相对路径名)。当用户登录到UNIX系统时,其当前工作目录通常是口令文件(/etc/passwd)中该用户登陆项的第6个字段--用户的其实目录(home directory)。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性。
进程调用chdir或fchdir函数可以更改当前工作目录。
#include <unistd.h>
int chdir(const char *pathname);
int fchdir(int fd);
两个函数返回值:若成功,返回0;若出错,返回-1;
因为当前工作目录是进程的一个属性,所以它只影响调用chdir的进程本身,而不影响其他进程,则下例得不到我们可能希望的结果。
#include <stdio.h>
#include <unistd.h>
int main(void)
{
    if (chdir("/tmp") < 0)
    {
        printf("chdir failed!\n");
    }
    printf("chdir to /tmp successed\n");
    return 0;
}
编译得到如下结果:
$ pwd
/usr/lib
$ mycd
chdir to /tmp successed
$ pwd
/usr/lib
从中可以看出,执行mycd命令的shell的当前工作目录并没有改变,这是shell执行程序工作方式的一个副作用。每个程序运行在独立的进程中,shell的当前工作目录并不会随着程序调用chdir而改变。由此可见,为了改变shell进程自己的工作目录,shell应当直接调用chdir函数,为此,cd命令内建在shell中。
因为内核必须维护当前工作目录的信息,所以我们应能过去其当前值。但是内核为每个进程只保存指向改目录v节点的指针等目录本身的信息,并不保存该目录吃的完整路径名。
我们需要一个函数,他从当前工作目录开始(.)开始,用..找到其上一级目录,然后读其目录项,直到该目录项中的i节点编号与工作目录i节点编号相同,这样就找到了其对应的文件名。 
函数getcwd就提供了这种功能。
#include <unistd.h>
char *getcwd(char *buf, size_t size);
返回值:若成功,返回buf,若出错,返回NULL
必须向此函数传递两个函数,一个是缓冲区地址buf,另一个是缓冲区的长度size(以字节为单位)。该缓冲区必须具有足够长的长度以容纳绝对路径名加null字节,否则返回出错。
以下程序将工作目录更改到一个指定目录,然后调用getcwd,最后打印该工作目录。
#include <unistd.h>
#include <stdio.h>
int main(void)
{
    char buffer[1024];
    size_t size;
    if (chdir("/usr/spool/uucpppublic") < 0)
    {
        printf("chdir failed\n");
    }
    if (getcwd(buffer, sizeof(buffer) == NULL)
    {
        printf("getcwd failed\n");
    }
    printf("cwd is: %s\n", buffer);
            
    exit(0);
}

chdir跟随符号链接(连接到实际的文件夹),但是当getcwd沿目录树上溯遇到/var/spool目录时,他并不了解该目录由符号链接/usr/spool所指向(getcwd并不知道/var/spool是由符号链接/usr/spool,它只知道/var/spool)。这是符号链接的一个特性 。
    fchdir函数提供了便捷的方法。在更换到文件系统中的不同位置前,无需调用getcwd函数,而是使用open打开当前工作目录,然后保存其返回的文件描述符。当希望回到原工作目录时,只要简单的将该文件描述符传送给fchdir。

二三、 设备特殊文件
st_dev和st_rdev这两个字段经常引起混淆,有关规则如下:
*每个文件系统所在的存储设备都由其主、次设备号表示。设备号所用的数据类型是基本系统数据类型dev_t。主设备号标识设备驱动程序,有时编码为于其通信的外设扮;次设备号标识特定的子设备。
*我们通常使用两个宏:major和minor来访问主、次设备号,大多数实现都定义这两个宏。这就意味着我们无需关心这两个数是如何存放在dev_t对象中的。
Linux将宏major和minor定义在头文件<sys/sysmacros.h>中,而该头文件又包括在<sys/type.h>中。
*系统中与每个文件名关联的st_dev值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的i节点。
只有字符特殊设备和块特殊设备才有st_rdev值。此指包含实际设备的设备号。
如下程序为每个命令行参数打印设备号,另外,若此参数引用的是字符特殊文件或块特殊文件,则还会打印该特殊文件的st_rdev值。

#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/sysmacros.h>
int main(int argc, char *argv[])
{
        int         i;
        struct stat buf;
        for(i=1; i<argc; i++)
        {
                printf("%s: ", argv[i]);
                if(stat(argv[i], &buf) < 0)
                {
                        err_ret("stat error");
                        continue;
                }
                printf("dev = %d/%d", major(buf.st_dev), minor(buf.st_dev));
                if(S_ISCHR(buf.st_mode) || S_ISBLK(buf.st_mode))
                {
                        printf(" (%s) rdev = %d/%d", 
                                (S_ISCHR(buf.st_mode)) ? "character" : "block",
                                major(buf.st_rdev), minor(buf.st_rdev));
                }
                printf("\n");
        }
        exit(0);
}
运行该程序,如下:

 注意,两个终端设备(st_dev)的文件名和i节点在设备0/5上(devtmpfs伪文件系统,他实现了/dev文件系统),但是他们的实际设备号是4/0和4/1。

二四、 文件访问权限小结



-------------------------------------------------------------------------华丽的风格线---------------------------------------------------------------------------------

QQ群:西安C/C++开发者,诚邀您的加入


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值