原文来自静雅斋,转载请注明出处。javascript
PS:原先写好文章了,结果备份Boom了,结果只好重写了一遍,文件系统部分原本是准备了图片的,可是如今没了。各位将就着看。java
The ISVTX (the sticky bit) indicates to the system which executable files are shareable (the default) and the system maintains the program text of the files in the swap area. The sticky bit may only be set by the super user on shareable executable files.
If mode ISVTX (the `sticky bit') is set on a directory, an unprivileged user may not delete or rename files of other users in that directory. The sticky bit may be set by any user on a directory which the user owns or has appropriate permissions. For more details of the properties of the sticky bit, see sticky(8).复制代码
黏着位是一个颇有意思的参数,S_ISVTX
其实是save text bit的缩写,咱们知道,一个二进制程序其实是由如下几部分组成node
正文段包含了程序几乎大部分的内容,并且因为正文段的特殊性,它是只读的,那么在早期Unix环境资源很是稀少的状况下,为了保证快速加载程序,就出现了保存正文位这一个特殊bit,若是一个程序的stx位被设置了,当程序第一次载入执行,在其终止时,程序正文段依旧会有一个副本被保存在交换区。在如今看来其实是一种缓存技术,Node.JS的模块加载机制就与此相似。可是因为如今的Unix系统虚拟内存技术、预存取技术的存在,这个机制已经被废弃了。
正如上面Unix系统手册上所说,当黏着位设置在目录上的时候,没有特权的用户就不能删除或者重命名其余用户在这个目录中的文件。很典型的案例就是/tmp
和/var/tmp
目录,tmp文件夹通常都是rwxrwxrwt的权限,由root用户拥有,根据规定,只有拥有文件、拥有目录、超级用户的三者之一才能删除或改名文件,这样就能保证了不会对其余用户的文件误操做。
从man 8 sticky
的手册页中咱们也能看到关于黏着位的描述shell
The sticky bit has no effect on executable files. All optimization on whether text images remain resident in memory is handled by the kernel's virtual memory system.复制代码
黏着位已经对二进制文件没有任何做用了,虚拟内存机制取代了一切。数组
int chown(const char *path, uid_t owner, gid_t group);
int fchown(int fildes, uid_t owner, gid_t group);
int lchown(const char *path, uid_t owner, gid_t group);
int fchownat(int fd, const char *path, uid_t owner, gid_t group, int flag);
DESCRIPTION
The owner ID and group ID of the file named by path or referenced by fildes is changed as specified by the arguments owner and group. The owner of a file may change the group to a group of which he or she is a member, but the change owner capability is restricted to the super-user.
The chown() system call clears the set-user-id and set-group-id bits on the file to prevent accidental or mischievous creation of set-user-id and set-group-id programs if not executed by the super-user. The chown() system call follows symbolic links to operate on the target of the link rather than the link
itself.
The fchown() system call is particularly useful when used in conjunction with the file locking primitives (see flock(2)).
The lchown() system call is similar to chown() but does not follow symbolic links.
The fchownat() system call is equivalent to the chown() and lchown() except in the case where path specifies a relative path. In this case the file to be changed is determined relative to the directory associated with the file descriptor fd instead of the current working directory.
Values for flag are constructed by a bitwise-inclusive OR of flags from the following list, defined in <fcntl.h>:
AT_SYMLINK_NOFOLLOW
If path names a symbolic link, ownership of the symbolic link is changed.
If fchownat() is passed the special value AT_FDCWD in the fd parameter, the current working directory is used and the behavior is identical to a call to chown() or lchown() respectively, depending on whether or not the AT_SYMLINK_NOFOLLOW bit is set in the flag argument.
One of the owner or group id's may be left unchanged by specifying it as -1.复制代码
上面是Mac OS X系统用户手册关于chown
函数的介绍,从它和原著内容的对照,能够发现,实际上原著上面提到的内容,均可以从系统手册上找到,笔者我的很是推崇Unix自带的系统手册,除了方便快捷,内容也很是的详细,能够说,原著就是讲系统手册内容组合起来,而后加上了做者本身的理解。
这个函数族也很是的简单,除了fchownat
函数多了一个flag参数之外,其余的参数都是同样的,从手册最后一句能够知道,owner
和group
两个参数任意一个为-1,就表明对应的ID不变。当文件是符号连接的时候,lchown
和flag
参数为AT_SYMLINK_NOFOLLOW的fchownat
是同样的行为,都是符号连接自己的全部者。
从手册中了解到,文件拥有者能够将组改成拥有者的其余附属组,可是只有root用户才能改变文件拥有者,原著中提到,基于BSD的系统一直规定只有超级用户才能更改文件全部者,Mac OS X系统也是BSD系的,因此有这规定不足为奇,而原著后面也提到了这个限制在FreeBSD8.0、Linux3.2.0和Mac OS X 10.6.8中一直存在,但是笔者在基于Linux2.6系列内核的CentOS6.x中发现,CentOS6.x实际上也有此限制,而且在用户手册上写到缓存
Only a privileged process (Linux: one with the CAP_CHOWN capability) may change the owner of a file. The owner of a file may change the group of the file to any group of which that owner is a member. A privileged process (Linux: with CAP_CHOWN) may change the group arbitrarily.复制代码
这点很是让人疑惑,因为笔者并无测试其余2.6.x内核的Linux发行版,因此不知道这个限制是Linux2.6就有的,仍是说是RedHat公司打的补丁。若是有朋友知道能够指正一下。
chown系统调用还会清除设置用户ID和设置组ID位,若是函数不是被root权限调用。cookie
stat
结构体中有一个成员变量st_size
用于表示以字节为单位的文件长度,咱们知道,Unix有7种文件数据结构
很容易就能想到这个字段只对普通文件、目录文件和符号连接才有意义,可是请注意,因为FIFO的特殊性,文件长度的存在是能让进程通讯更加便利,因此在在现代Unix系统中,FIFO也有这一属性。
你们应该还记得stat
结构体中的st_blksize
和st_blocks
属性,这两个属性就是关于文件在文件系统中占据空间的属性,第一个是最优文件系统块大小,第二个是分配给该文件的磁盘块数。
在前面的例程中,提到了普通文件存在文件空洞,也解释了文件空洞存在的缘由,当文件长度小于所占用的磁盘块大小,就说明存在着文件空洞。当咱们使用cat
命令复制存在空洞的文件,新的文件就不存在空洞了,全部的空洞都会被填满。更有意思的是,使用od
命令去查看这两个文件的二进制内容,能够发现空洞和新文件被填满的部分都是以null的形式(0字节)存在,app
在不少时候,开发者须要将文件截断一部分用于缩短文件。一个很典型的案例就是将文件截断为0,也就是使用O_TRUNC
参数打开文件,可是系统也提供了两个函数用于文件的截断less
int truncate(const char *path, off_t length);
int ftruncate(int fildes, off_t length);复制代码
这两个函数能够将文件截断也能够扩展,若是length大于文件长度,那么就会扩展当前的文件,而且扩展的空间将以文件空洞的形式存在。
并且注意,这两个函数不会修改当前文件的偏移量,若是不注意这一点,颇有可能形成偏移量超出了新的文件长度,从而造成空洞。
文件系统是一个很是庞大的模型,笔者在这里讲本身的理解,若是有须要更深一步了解的朋友,能够自行观看原著或者《鸟哥的Linux私房菜》中有关文件系统的部分。
目前Unix系统几乎都有不少本身的文件系统实现,每种文件系统都有其本身的特殊性,可是对于大部分文件系统来讲,都是基于同一个模型创建的,因此实际上很类似。
文件系统有不少部分组成,广泛的实现中,都是将实际文件数据和文件属性分开,分别存放。这就是数据块和索引块,一般也叫inode块,还有包含了整个文件系统属性的超级块,为了方便管理,一些文件系统还将块进行分组,造成块组,可是这个概念实际上可有可无。对于开发者来讲重要的是索引块和数据块。对于用户来讲,使用ls
命令查看当前目录下的全部文件,一般能够看到以下内容
drwxr-xr-x 9 uid gid 306 2 3 18:14 .
drwxr-xr-x 11 uid gid 374 2 3 18:12 ..
-rw-r--r-- 1 uid gid 2290 2 3 18:13 error.c
-rw-r--r-- 1 uid gid 2229 2 3 18:13 errorlog.c
drwxr-xr-x 4 uid gid 136 2 3 18:12 include复制代码
有文件有目录,还有两个.
、..
特殊文件,实际上ls
命令并无检索真正的数据块,这些项目就是索引块体现的,每一个文件都对应了一个独一无二的索引块,可是并非每一个文件都有独一无二的数据块。索引块中记录了文件的基本信息,包括权限、大小、类型等等,上面第二列是项目的连接计数,也就是stat
结构体中的st_nlink
成员。咱们知道,Unix的连接分为两种,硬连接和软连接(符号连接),每一个inode块都有连接计数,只有当连接计数为0的时候,文件系统才真正的删除数据块,这一点和C++中的引用计数很是类似。
软连接其实是一种文件,它存在着本身的索引块和数据块,并且软连接的存在不会让索引计数改变,只是数据块内容是另外一个文件的位置,这点和C语言中的指针很是类似。
当使用mkdir
建立目录的时候,能够发现新目录的引用必然是2,由于目录下默认就有两个特殊文件.
和..
,目录自己指向自身,.
文件也指向自身,因此新目录索引计数必然为2.
从上面的信息能够知道,因为硬连接是文件系统inode块产生的,那么硬连接就必然没法跨越文件系统,而软连接是实实在在的文件,因此软连接能够跨文件系统。而且,这就是为何系统提供的是unlink
函数而不是delete
函数的缘由。
int link(const char *path1, const char *path2);
int linkat(int fd1, const char *name1, int fd2, const char *name2, int flag);
int unlink(const char *path);
int unlinkat(int fd, const char *path, int flag);复制代码
系统提供了上面四个函数用于连接的建立和释放,具体的介绍则在上一节讲述了。通过了这么多函数的介绍,想必你们也对Unix函数的风格有所了解了。和其余的Unix函数同样,文件描述符能够用AT_FDCWD
来代替,这表明着路径将以当前工做目录来计算。
linkat函数的flag参数只有AT_SYMLINK_FOLLOW
可用,这和以前咱们所遇到的函数都不大同样,当这个参数存在时,函数将建立指向连接目标的连接,不然建立指向符号连接自己的连接。
unlinkat的flag则只有AT_REMOVEDIR
可用,当此参数被传入时,unlinkat将和rmdir同样删除目录。
#include "include/apue.h"
#include <fcntl.h>
int main(int argc, char *argv[])
{
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);
}复制代码
The unlink() function removes the link......and no process has the file open
这是Unix手册中的一句话,当文件被进程打开时,unlink函数不会删除连接,因此这个特性在实际开发中十分实用,能够用来保证进程崩溃时,删除临时文件。
并且在ISO C标准函数库中,有一个remove
函数,一样也是用于解除连接。
int rename(const char *old, const char *new);
int renameat(int fromfd, const char *from, int tofd, const char *to);
DESCRIPTION
The rename() system call causes the link named old to be renamed as new. If new exists, it is first removed. Both old and new must be of the same type(that is, both must be either directories or non-directories) and must reside on the same file system.
The rename() system call guarantees that an instance of new will always exist, even if the system should crash in the middle of the operation.
If the final component of old is a symbolic link, the symbolic link is renamed, not the file or directory to which it points.
The renameat() system call is equivalent to rename() except in the case where either from or to specifies a relative path. If from is a relative path, the
file to be renamed is located relative to the directory associated with the file descriptor fromfd instead of the current working directory. If the to is a
relative path, the same happens only relative to the directory associated with tofd. If the renameat() is passed the special value AT_FDCWD in the fromfd or tofd parameter, the current working directory is used in the determination of the file for the respective path parameter.复制代码
因为这个函数原著已经讲得足够详细,因此这里只补充如下几点
符号连接,即一般说的软连接,是对一个文件的间接指针。硬连接是直接指向索引块,而软连接其实是一个文件,其实从上面你们应该看到了硬连接的局限和危险,直接操做硬连接后果不好,因此引入符号连接就是为了解决硬连接存在的问题。
因为符号连接和普通文件几乎同样,因此任何涉及文件的函数,都须要注意是否处理符号连接,通常来讲都是以flag参数的形式来改变函数的行为,从前面这么多的函数应该了解到了。
Unix系统也提供了建立符号连接的函数
int symlink(const char *path1, const char *path2);
int symlinkat(const char *name1, int fd, const char *name2);复制代码
函数很简单,两个函数就只有相对路径和绝对路径的区别。因为符号连接自己是一个文件,因此也应该能够读写,可是open函数是跟随符号连接的,因此Unix系统也提供了打开符号连接的函数
ssize_t readlink(const char *restrict path, char *restrict buf, size_t bufsize);
ssize_t readlinkat(int fd, const char *restrict path, char *restrict buf, size_t bufsize);复制代码
这两个函数实际上就是一个相对路径和绝对路径的区别,并且这两个函数都整合了open、read、close的操做
在前面的文章里讲到了stat结构体,而且能够看到,stat结构体中时间字段有秒和纳秒两种精度,可是从上面讲的文件系统能够知道,inode块记录了文件的详细信息,天然也包括了文件的时间信息,而inode块的实现是根据文件系统来肯定的,这意味着精度是根据文件系统的不一样而不一样。
在学习Unix系统的时候你们应当知道了文件有三个时间
|字段|说明|例子|ls(1)选项|
|----|---|---|--------|
|st_atime|文件的最后访问时间|read|-u|
|st_mtime|文件的最后修改时间|write|默认|
|st_ctime|inode块状态的最后更改时间|chmod、chown|-c|
从上面咱们知道,如今大多数的文件系统索引块和数据块是分别存放的,也就是说,修改文件信息和修改文件内容彻底是两码事,当执行影响inode块的命令的时候就会修改st_ctime
,当执行内容修改的时候就会修改st_mtime
咱们知道,目录也是一种文件,增长、删除或修改目录项会影响到它所在的目录的相关的三个时间,通常有如下几点规律
其实最关键的就是理解文件系统索引块和数据块分离的事实,这样就很容易理解上面的概念了。
顺便提一句,atime是access time、mtime是modification time、ctime是change time,有一些Linux教程书里面将ctime认为是文件内容更改时间,是错误的。
int utimes(const char *path, const struct timeval times[2]);
int futimes(int fildes, const struct timeval times[2]);复制代码
为了管理文件时间,Unix系统也提供了文件时间的管理函数,在原著中,有三个函数提供,可是很是惋惜的是,Mac OS X系统只提供了以上两个函数,utimensat
函数并未提供,而且futimens
被futimes
替代了,而Linux系统则是提供utimensat
函数的。
int utimensat(int dirfd, const char *pathname, const struct timespec times[2], int flags);
If times is NULL, the access and modification times are set to the current time. The caller must be the owner of the file, have permission to write the file, or be the super-user.
If times is non-NULL, it is assumed to point to an array of two timeval structures. The access time is set to the value of the first element, and the modi-fication time is set to the value of the second element. The caller must be the owner of the file or be the super-user.
In either case, the inode-change-time of the file is set to the current time.复制代码
上面的是CentOS6.x系统提供的函数原型,经过对比以前那些函数族,能够发现,实际上系统应当提供这一功能强大的方法,具体缘由并不清楚,若是有朋友了解能够指点一二。
这两个参数都是传入一个timeval
结构体数组,timeval
其实是如下结构体
_STRUCT_TIMEVAL
{
__darwin_time_t tv_sec; /* seconds */
__darwin_suseconds_t tv_usec; /* and microseconds */
};复制代码
实际上这个和timespec
结构体是同样样的,笔者也不知道为何已经有了timespec
还要再搞一个结构体出来。这个参数的两个时间都是日历时间,也就是俗称的Unix时间戳,一个是acess time
一个是modification time
,具体规则上面实际上已经有了。
原著中实际上写了不少,可是很惋惜,系统手册上只有这么点东西,笔者这里就只按照系统手册来写,若是有朋友想要了解原著内容请查看原著。
而后下面是原著中的一个例程,这个程序其实是无法跑的,因此在这里笔者对其修改了。
#define _DARWIN_C_SOURCE 1
#include "include/apue.h"
#include <sys/time.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int i, fd;
struct stat statbuf;
struct timeval times[2];
for (i = 1; i < argc; ++i) {
if (stat(argv[i], &statbuf) < 0) {
err_ret("%s: stat error", argv[i]);
continue;
}
if ((fd = open(argv[i], O_RDWR | O_TRUNC)) < 0) {
err_ret("%s: open error", argv[i]);
continue;
}
times[0].tv_sec = statbuf.st_atimespec.tv_sec;
times[0].tv_usec = statbuf.st_atimespec.tv_nsec;
times[1].tv_sec = statbuf.st_mtimespec.tv_sec;
times[1].tv_usec = statbuf.st_mtimespec.tv_nsec;
if (futimes(fd, times) < 0)
err_ret("%s: futimens error", argv[i]);
close(fd);
}
exit(0);
}复制代码
苹果系统内的futimes
和utimes
函数是归类给标准C库的,可是确实放在BSD系统调用中的,不要问我为何,由于我也不知道为何,并且能够经过查看<sys/stat.h>
头文件发现,程序所须要的结构体定义是放在#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
,因为APUE头文件已经定义了_POSIX_C_SOURCE
,因此必需要增长_DARWIN_C_SOURCE
宏定义,不增长的后果自行尝试,并且因为标准C库本身一套数据结构,必须在这里手工转换timespec->timeval
类型。实际上两个是同样的。因为系统提供精确到纳秒的时间,而标准C库提供的是精确到毫秒的时间,因此须要单位转换。
上面说了那么多,实际上并无什么卵用,这个程序只是为了证实修改文件信息会致使st_ctime
的时间改变罢了。
int mkdir(const char *path, mode_t mode);
int mkdirat(int fd, const char *path, mode_t mode);复制代码
没什么好说的,因为前面说过的BSD系列的特色,新目录的拥有者ID是进程的有效拥有者ID,组ID则是父目录的组ID,并且还有一点须要注意,mkdir函数对mode
超出低九位的行为是没有反应的,因此最好在mkdir后使用chmod确认权限。
int rmdir(const char *path);复制代码
前面说过,目录是一个文件,因此它也具备文件的特色,例若有进程打开时,删除会执行,可是不会释放实际目录,这个函数只能用来删除空目录
对于用户来讲,目录的读写是透明的,可是实际上只有内核才能写目录,这样就不会出现文件系统的混乱,POSIX标准定义了一套有关目录的函数
DIR *opendir(const char *filename);
DIR *fdopendir(int fd);
struct dirent *readdir(DIR *dirp);
long telldir(DIR *dirp);
void seekdir(DIR *dirp, long loc);
void rewinddir(DIR *dirp);
int closedir(DIR *dirp);复制代码
这里只讨论原著中的函数原型,头两个很简单,就是打开一个目录返回DIR *
在第一篇文章中有一个ls
程序的实现,苹果系统下的实现以下
struct dirent {
ino_t d_ino; /* file number of entry */
__uint16_t d_reclen; /* length of this record */
__uint8_t d_type; /* file type, see below */
__uint8_t d_namlen; /* length of string in d_name */
char d_name[__DARWIN_MAXNAMLEN + 1]; /* name must be no longer than this */
};复制代码
Unix规范规定,至少必须包含d_ino
和d_name
实现,DIR
结构体的实现以下
typedef struct {
int __dd_fd; /* file descriptor associated with directory */
long __dd_loc; /* offset in current buffer */
long __dd_size; /* amount of data returned */
char *__dd_buf; /* data buffer */
int __dd_len; /* size of data buffer */
long __dd_seek; /* magic cookie returned */
long __dd_rewind; /* magic cookie for rewinding */
int __dd_flags; /* flags for readdir */
__darwin_pthread_mutex_t __dd_lock; /* for thread locking */
struct _telldir *__dd_td; /* telldir position recording */
} DIR;复制代码
是否是和FILE
结构体很像,这个结构体实际上才是保存了目录信息。打开的两个函数返回的DIR
最终是被其余函数所使用,并且咱们看到,目录实际上也有偏移量一说。readdir
函数返回下一个目录项的指针,直到最终到底部返回null。telldir
是查询偏移量,seekdir
是设置偏移量,rewinddir
是将偏移量放到最开始,closedir
就是关闭目录。
原著还写了一个例程用于遍历目录层次,可是笔者认为并无什么必要,若是还有不明白的彻底能够查询Unix用户手册,因此这里就再也不赘述。
每一个进程实际上都有一个当前工做目录,这个目录在前面不少函数中都很是有用,全部的相对路径都是以此计算,当前工做目录是进程一个属性,Unix系统也提供了系统函数。
int chdir(const char *path);
int fchdir(int fildes);复制代码
咱们知道,shell实际上也是一个进程,它在启动时读取/etc/passwd
中有关登陆用户的家目录的字段,而后将自身工做目录改变到家目录。
笔者在这里强调,工做目录是进程一个属性,它和其余进程无关,这样咱们就理解了为什么cd
命令是shell内建命令。
并且咱们知道,子进程会继承父进程的属性,因此shell打开一个程序,它的初始工做目录就是shell的工做目录。
讲到这里,可能有些朋友已经以为奇怪,工做目录既然是一个属性,为何没有获取当前工做目录完整路径的函数,其实是这样,因为内核的实现中,内核只保存了当前工做目录的一个指针,并无保存完整的路径名,因此没有直接获得当前工做目录路径的函数。
可是因为内核持有当前工做目录的指针,那么,是否是能够经过反向向上查找,一级级反推,最终组装出当前工做目录呢。答案是能够的,并且系统还将其封装好了。
char *getcwd(char *buf, size_t size);
char *getwd(char *buf);复制代码
这两个是标准C函数,第二个函数其实是一个兼容性函数,不须要提供缓冲区大小指示。
#include "include/apue.h"
int main(int argc, char *argv[])
{
char *ptr;
size_t size;
if (chdir("/var") < 0)
err_sys("chdir error");
ptr = path_alloc(&size);
if (getcwd(ptr, size) == NULL)
err_sys("getcwd failed");
printf("cwd = %s\n", ptr);
exit(0);
}复制代码
这是原著一个例程,在这里有一个path_alloc
函数,也是须要咱们手动将其编译为静态库,而后连接到程序中。为了保证Mac OS X系统能运行,这里对目录作了修改,将其指定为/var
。
> ./a.out
cwd = /private/var
> ls -l /var
lrwxr-xr-x 1 root wheel 11 2 2 03:17 /var -> private/var复制代码
因此能够知道chdir是跟随符号连接的,可是当getcwd沿着目录向上时,仍是会根据实际路径计算。
在getcwd
说明中,系统还教了一种简便方法
A much faster and less error-prone method of accomplishing this is to open the current directory (`.') and use the fchdir(2) function to return.复制代码
在更换到其余目录前,直接使用open
打开当前工做目录(.),而后保存文件描述符,当但愿回到原工做目录时,只须要将其传递给fchdir
就好了。
原著讲的很详细了,并且这个并非很是有用,因此这里就不说起了。