这里对应的是原书的第4章(文件和目录)
文件和目录
这一章主要是围绕打开文件,读写文件。
先从stat函数开始,stat系列函数用来返回一个文件的信息,放在一个stat结构中
大部分信息都能够通过ls -l命令查看
stat函数有四个变种,分别是
// 返回0表示成功,返回-1表示错误
#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 fd, const char *restrict pathname, struct stat *restrict buf, int flag);
// stat结构实际定义可能不同,但是基本的形式是
struct stat {
mode_t st_mode; /* 文件的类型和访问权限 */
ino_t st_ino; /* 文件的i节点号 */
dev_t st_dev; /* 设备序号 */
dev_t st_rdev; /* 特殊文件的序号 */
nlink_t st_nlink; /* 链接的数量 */
uid_t st_uid; /* 文件拥有者的uid */
gid_t st_gid; /* 文件拥有者的gid */
off_t st_size; /* 普通文件的话,这个字段表示文件的大小 */
struct timespec st_atim; /* 最后一次访问文件的时间 */
struct timespec st_mtim; /* 最后一次修改文件的时间 */
struct timespec st_ctim; /* 最后一次文件状态改变的时间 */
blksize_t st_blksize; /* 最佳 I/O 块大小 */
blkcnt_t st_blocks; /* 分配的磁盘块数 */
};
我们介绍几个主要的字段
1、文件类型
文件类型包括以下几种
普通文件。这种就是常用的文件,至于这些数据是文本还是二进制数据对unix内核没有区别。对普通文件内容的解释由处理该文件的应用程序进行目录文件。这种文件包含了其他文件的名字以及指向与这些文件直接有关信息的指针。对一个目录有读文件的任一进程都可以读取该目录的内容,但只有内核可以直接写目录文件。块特殊文件。这种类型的文件提供对设备(如磁盘)带缓冲的访问,每次以固定长度为单位进行。字符特殊文件。这种类型的文件提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件FIFO。具名管道,这种类型的文件用于进程间通信socket。套接字,用于进程间的网络通信符号链接。这种类型的文件指向另一个文件
可以用宏来判断一个stat结构中的st_mode字段来判断文件的类型
| macro | type of file |
|---|---|
| S_ISREG() | 普通文件 |
| S_ISDIR() | 目录 |
| S_ISCHR() | 字符特殊文件 |
| S_ISBLK() | 块特殊文件 |
| S_ISFIFO() | 管道 |
| S_ISLNK() | 符号链接 |
| S_ISSOCK() | socket |
| S_TYPEISMQ() | 消息队列 |
| S_TYPEISSEM() | 信号量 |
| S_TYPEISSHM() | 共享内存 |
让我们看一下书里的一个案例代码
#include "apue.h"
int main(int argc, char *argv[])
{
int i;
struct stat buf;
char *ptr;
for (i = 1; i < argc; i++) {
printf("%s: ", argv[i]);
if (lstat(argv[i], &buf) < 0) {
err_ret("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);
}
这段程序的意思大概就是打印命令行参数指定的文件,用lstat这样如果打开符号链接,打开的是它指向的那个文件
让我们先编译出可执行文件再测试(我在ubuntu上找不到管道的目录所以比起原书差了一个)
$ gcc filetype.c -lapue
$ ./a.out /etc/passwd /etc /dev/log /dev/tty /dev/sda /dev/stdin
访问权限
与一个进程相关的ID有6个或者更多 实际用户ID,实际组ID,有效用户ID,有效组ID,附属组ID,保存的设置用户ID,保存的设置组ID 设置文件的用户ID和组ID都包含在stat结构中的st_mode中 st_mode也包含了对文件的访问权限位。 每个文件有9个访问位
函数access和faccessat
access和faccessat按照实际用户ID和实际组ID进行权限测试
#include <unistd.h>
int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
// 若成功返回0,出错返回-1
mode标志
如果pathname参数是绝对目录,或者fd是AT_FDCWD而pathname是相对参数。则这两个函数的作用相同 flag用来改变faccessat函数的行为,如果flag设置为AT_EACCESS,访问检查用的是进程的有效用户ID和有效组ID,而不是实际用户ID和实际组ID。 来看书上的一个例子
#include "apue.h"
#include <fcntl.h>
int main(int argc, char *argv[])
{
if (argc != 2)
err_quit("usage: a.out <pathname>");
if (access(argv[1], R_OK) < 0)
err_ret("access error for %s", argv[1]);
else
printf("read access OK\n");
if (open(argv[1], O_RDONLY) < 0)
err_ret("open error for %s", argv[1]);
else
printf("open for reading OK\n");
exit(0);
}
让我们编译调试书上的例子
$ gcc access.c -lapue
$ ls -l a.out
-rwxrwxr-x 1 yuanzhihong yuanzhihong 17584 Jun 5 21:16 a.out
$ ./a.out a.out
read access OK
open for reading OK
$ ls -l /etc/shadow
-rw-r----- 1 root shadow 1085 Jun 5 21:15 /etc/shadow
$ ./a.out /etc/shadow
access error for /etc/shadow: Permission denied
open error for /etc/shadow: Permission denied
$ su root
Password:
# ./a.out /etc/shadow
read access OK
open for reading OK
# chown root a.out
# chmod u+s a.out
# su yuanzhihong
$ ls -l a.out
-rwsrwxr-x 1 root yuanzhihong 17584 Jun 5 21:16 a.out
$ ./a.out /etc/shadow
access error for /etc/shadow: Permission denied
open for reading OK
在这个例子中,open能够打开文件,但是设置文件的用户ID,程序会认为实际用户不能正常读指定的文件
umask函数
umask为进程设置文件模式创建屏蔽字,并返回原来的文件屏蔽字
#include <sys/stat.h>
mode_t umask(mode_t cmask);
// 返回原来的文件屏蔽字,其中的cmask参数是S_IRUSR等常量的按位或
在进程创建一个文件或者目录时,就会使用文件模式创建屏蔽字。open和creat函数有一个参数mode就是来指定屏蔽字的 让我们来看书里的例子,创建两个文件,一个叫foo一个叫bar。创建第一个文件时,umask值为0,第二个文件时umask禁止组和其他用户的访问权限
#include "apue.h"
#include <fcntl.h>
#define RWRWRW (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)
int main(void)
{
umask(0);
if (creat("foo", RWRWRW) < 0)
err_sys("creat 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);
}
我们来编译测试一下
$ gcc umask.c -lapue
$ umask
022
$ ./a.out
$ ls -l foo bar
-rw------- 1 yuanzhihong yuanzhihong 0 Jun 5 21:36 bar
-rw-rw-rw- 1 yuanzhihong yuanzhihong 0 Jun 5 21:36 foo
$ umask
022
从中可见,更改进程的文件模式创建屏蔽字并不影响其父进程的屏蔽字
chmod系列函数
chmod系列函数用来改变现有文件的访问权限
#include <sys/stat.h>
int chmode(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
int fchmodat(int fd, const char *pathname, mode_t mode, int flag);
// 如果成功,返回0,出错返回-1
chmod在指定的文件上进行操作,而fchmod则对已经打开的文件进行操作。fchmodat函数与chmod在以下两种情况是相同的:一种是pathname为绝对目录,一种是fd参数取值为AT_FDPWD而pathname参数为相对路径。flag参数可以改变fchmodat的行为,当设置了AT_SYMLINK_NOFOLLOE时,将不追随符号链接 为了改变一个文件的权限位,进程的有效用户ID必须等于文件的所有者ID,或者该进程必须具有超级用户权限。
让我们看书上的一个例子
#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);
}
让我们编译测试这个程序
$ gcc changemod.c -lapue
$ ls -l foo bar
-rw------- 1 yuanzhihong yuanzhihong 0 Jun 5 21:36 bar
-rw-rw-rw- 1 yuanzhihong yuanzhihong 0 Jun 5 21:36 foo
$ ./a.out
$ ls -l foo bar
-rw-r--r-- 1 yuanzhihong yuanzhihong 0 Jun 5 21:36 bar
-rw-rwSrw- 1 yuanzhihong yuanzhihong 0 Jun 5 21:36 foo
chown系列函数
chown系列函数用来改变文件的用户ID和组ID。如果两个参数 owner和group中的任意一个为-1,则对应的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 fchwonat(int fd, const char *pathname, uid_t owner, gid_t group);
int lchwon(const char *pathname, uid_t owner, gid_t group);
// 返回0表示成功,返回-1表示错误
截断函数
为了截断文件可以使用truncate函数和ftruncate函数
#include <unistd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
// 若成功,返回0;失败返回-1
如果以前的长度小于指定的length,那么文件长度要增加;如果文件变长以前的文件的尾端和新的文件尾端之间被0填充。
文件系统
可以把一个磁盘分成一个或者多个分区。每个分区可以包含一个文件系统。节点是固定长度的记录项,它包含有关文件的大部分信息。
link系列函数
任何一个文件可以有多个目录项指向其i节点。创建一个指向现有文件的链接的方法是使用link函数或者linkat函数
#include <unistd.h>
int link(const char *pathname, const char *newpath);
int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flag);
// 成功返回0,失败返回-1
这两个函数创建一个新目录项 newpath,它引用现有文件existingpath。如果newpath已经存在,则返回出错。只创建newpath中的最后一个分量,路径中的其他部分应当已经存在。 对于linkat函数,现有文件是通过efd和existingpath参数指定的,新的路径名是通过nfd和newpathname指定的。默认情况下,如果两个路径名中的任一个是相对路径,那么它需要通过相对应的文件描述符进行计算。如果两个文件描述符中的任一个设置为AT_FDCWD,那么相应的路径名就通过相对应当前目录进行计算。如果任一路径名是绝对路径,相应的文件描述符参数就会被忽略。 当现有文件是符号链接时,由flag参数来控制linkat函数是创建指向现有符号链接的链接还是创建指向现有符号链接目标的链接。如果flag中设置了AT_SYMLINK_FOLLOW标志,则创建指向符号链接目标的链接。如果这个标志被清除了,则创建一个指向符号链接本身的链接 为了删除一个现有的目录项,可以调用unlink函数
#include <unistd.h>
int unlink(const char *pathname);
int unlinkat(int fd, const char *pathname, int flag);
// 若成功,返回0;失败返回-1
这两个函数删除目录项,并将pathname所引用的文件的链接计数减1。如果对该文件还有其他链接,则任然可以通过其他链接访问该文件的数据。如果出错,则不对该文件做任何更改。 为了解除对文件的链接,必须对该目录项具有写和执行权限。 如果pathname为相对路径,那么unlinkat函数计算相对于fd文件描述符参数代表的目录的路径名。如果fd参数设置为AT_FDCWD,那么通过相对于调用进程的当前工作目录来计算路径名。如果pathname是绝对路径名,那么fd参数被忽略。flag参数给出了一种方式可以改变unlinkat的默认行为。当AT_REMOVEDIR标志被设置时,unlinkat函数可以类似于rmdir删除目录。如果这个标志被清除,unlinkat与unlink执行同样的操作。
让我们来看书上的例子
#include "apue.h"
#include <fcntl.h>
int main(void)
{
if (open("tempfile", O_RDWR) < 0)
err_sys("open error");
if (unlink("tempfile") < 0)
err_sys("unlink error");
printf("file unlinked\n");
sleep(15);
printf("done\n");
exit(0);
}
我们来编译调试这个程序
$ gcc unlink.c -lapue
$ ls -l tempfile
-rw-r--r-- 1 yuanzhihong yuanzhihong 242 Jun 6 10:44 tempfile
$ df /home
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sdb 263174212 5544872 244191184 3% /
$ ./a.out& ##在后台执行
[1] 2947
file unlinked
$ ls -l tempfile ##文件已经删除
ls: cannot access 'tempfile': No such file or directory
$ df /home ##可以看到可利用的空间变多了,我使用的ubuntu跟原书不一样
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sdb 263174212 5544920 244191136 3% /
done ##程序执行完毕
[1] + 2947 done ./a.out
$ df /home
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sdb 263174212 5544920 244191136 3% /
unlink的这种特性常常用来确保就算程序崩溃时,它所创建的临时文件也不会遗留下来。只有当进程关闭该文件或终止时,该文件的内容才会被删除(跟操作系统有关)
我们也可以使用remove函数解除对一个文件或者目录的链接。对于文件,remove的功能与unlink相同。对于目录,remove的功能与rmdir相同
#include <stdio.h>
int remove(const char *pathname);
//成功的话返回0,失败的哈返回-1
rename函数
文件可以使用rename函数或者renameat函数进行重命名
#include <stdio.h>
int rename(const char *oldname, const char *newpathname);
int renameat(int oldfd, const char *oldpathname, int newfd, const char *newname);
如果newname已经存在,而且不是一个目录,则先将该目录项删除再将oldname重命名为newname。如果是引用的符号链接,则处理的符号链接本身,不是链接的文件
创建和读取符号链接
#include <unistd.h>
int symlink(const char *actualpath, const char *sympath);
int symlinkat(const char *actualpath, int fd, const char *sympath);
// 成功返回0,出错返回-1
ssize_t readlink(const char *restrict pathname, char *restrict buf, size_t bufsize);
ssize_t readlinkat(int fd, const char *restrict pathname, char *restrict buf, size_t bufsize);
// 读取符号链接本身
// 若成功,返回读取的字节数;若出错,返回-1
文件的时间
每个文件维护三个时间字段
更改文件访问和修改时间的函数
#include <sys/stat.h>
int futimens(int fd, const struct timespec times[2]);
int utimensat(int fd, const char *path, const struct timespec times[2], int flag);
#include <sys/time.h>
int utime(const char *pathname, const struct timeval times[2]);
// 成功返回0;失败返回-1
// times的第一个元素包含访问时间,第二个元素包含修改时间
让我们来看书中的一个例子 先取得文件的最后修改和访问时间,然后截断,再将一开始的时间重置回去
#include "apue.h"
#include <fcntl.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) { /* fetch current times */
err_ret("%s: stat error", argv[i]);
continue;
}
if ((fd = open(argv[i], O_RDWR | O_TRUNC)) < 0) { /* truncate */
err_ret("%s: open error", argv[i]);
continue;
}
times[0] = statbuf.st_atim;
times[1] = statbuf.st_mtim;
if (futimens(fd, times) < 0) /* reset times */
err_ret("%s: futimens error", argv[i]);
close(fd);
}
exit(0);
}
我们来编译调试这个程序
$ ls -l changemod.c
-rw-r--r-- 1 yuanzhihong yuanzhihong 429 Jun 5 21:48 changemod.c
$ ls -lu changemod.c
-rw-r--r-- 1 yuanzhihong yuanzhihong 429 Jun 5 21:49 changemod.c
$ date
Sun Jun 6 11:24:42 CST 2021
$ gcc zap.c -lapue
$ ./a.out changemod.c
$ ls -l changemod.c
-rw-r--r-- 1 yuanzhihong yuanzhihong 0 Jun 5 21:48 changemod.c
$ ls -lu changemod.c
-rw-r--r-- 1 yuanzhihong yuanzhihong 0 Jun 5 21:49 changemod.c
$ ls -lc changemod.c
-rw-r--r-- 1 yuanzhihong yuanzhihong 0 Jun 6 11:25 changemod.c
正如我们所见,最后修改时间和最后访问时间未变。但是,状态更改时间则更改为程序运行时的时间
目录相关函数
用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
#include <unistd.h>
int rmdir(const char *pathname);
// 可以删除一个空目录,成功返回0,出错返回-1
读目录 由opendir或者fdopendir返回的指向DIR结构的指针由另外五个函数使用。readdir返回目录中的第一个目录项。
#include <dirent.h>
DIR *opendir(const char *pathname);
DIR *fdopendir(int fd);
struct dirent *readdir(DIR *dp);
//成功返回指针,出错返回NULL
void rewinddir(DIR *dp);
int closedir(DIR *dp);
//成功返回0,出错返回-1
long telldir(DIR *dp);
void seekdir(DIR *dp, long loc);
让我们来看书中的一个例子
#include "apue.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("FIFOs = %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 that 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 /* we return whatever func() returns */
myftw(char *pathname, Myfunc *func)
{
fullpath = path_alloc(&pathlen); /* malloc PATH_MAX+1 bytes */
/* ({Prog pathalloc}) */
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 /* we return whatever func() returns */
dopath(Myfunc* func)
{
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)
return(func(fullpath, &statbuf, FTW_DNR));
while ((dirp = readdir(dp)) != NULL) {
if (strcmp(dirp->d_name, ".") == 0 ||
strcmp(dirp->d_name, "..") == 0)
continue; /* 忽略 "." 和 ".." */
strcpy(&fullpath[n], dirp->d_name); /* 在 "/" 之后追加*/
if ((ret = dopath(func)) != 0) /* 递归使用 */
break;
}
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("unknown type %d for pathname %s", type, pathname);
}
return(0);
}
其中有关目录的一段是(也是最难的)
编译调试一下这个程序
$ gcc ftw8.c -lapue
$ ./a.out ../../apue.3e
regular files = 368, 60.43 %
directories = 33, 5.42 %
block special = 0, 0.00 %
char special = 0, 0.00 %
FIFOs = 0, 0.00 %
symbolic links = 208, 34.15 %
sockets = 0, 0.00 %
chdir系列函数
每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点。进程调用chdir或者fchdir函数更改当前工作目录。
#include <unistd.h>
int chdir(const char *pathname);
int fchdir(int fd);
// 返回0表示成功,返回-1表示失败
看书中的一个例子
#include "apue.h"
int main(void)
{
if (chdir("/tmp") < 0)
err_sys("chdir failed");
printf("chdir to /tmp succeeded\n");
exit(0);
}
编译调试一下
$ gcc mycd.c -lapue
$ pwd
/home/yuanzhihong/learing/apue/apue.3e/filedir
$ ./a.out
chdir to /tmp succeeded
$ pwd
/home/yuanzhihong/learing/apue/apue.3e/filedir
可以看到。执行mycd命令的shell的当前工作目录并没有改变
#include <unistd.h>
char *getcwd(char *buf, size_t size);
//得到当前的绝对目录,成功返回buff,失败返回NULL