Unix原理与应用(第四版)学习笔记2--系统调用之文件篇

时间:2022-09-29 10:27:30

文件篇

Unixc语言的系统调用库,Unix是一个多道程序系统,一个资源可能有多个进程共享使用。作为Unix系统开发者,必须将c语言的知识应用到程序设计中,并根据实际的需要,在不引起冲突的情况下,让多个应用程序共享资源。

5.1系统调用基础

Unix系统大概提供了200个特殊函数,这些函数为系统调用。系统调用是嵌在内核里的历程,它执行非常基本的功能,包括访问CPU(CPU的条件码和寄存器文件),内存外部设备。通常系统调用是使用汇编语言编写的,但是提供了C语言函数接口,接受各种类型的参数(通常个数不超过5,数据类型包含int,struct,pointer)。运行在Unix上的c程序可以按函数的格式调用这些系统调用。Unix系统和Linux系统的共同的特性:系统调用是相同的。(POSIX标准和单一UNIX规范的功劳)

当我们执行一个C程序时,CPU运行在用户模式下直到出现一个系统调用。用户模式下,进程只能访问有限的内存空间,执行有限的计算机指令.内核模式可以访问任何模式,执行任何机器指令,特别是一些I/O指令。(备注:最近由于看了《Orange‘s一个操作系统的设计与实践》对用户模式和内核模式理解加深了,实际上就是LDTGDT的断描述符,在访问控制时使用CPL-Current  Privileage Level,DPL- Descriptor  Privileage Level,RPL-Required Privileage Level 来控制权限并阻止非法访问)

5.2系统调用和库函数

库函数是建立在系统调用上的,由于库函数没有提供像系统调用那样的严格控制,所以还是需要使用系统调用的。然而,系统调用需要额外的开销和模式切换(特别的进程上下文的切换),因此需要谨慎的使用这些系统调用。

ANSIC引入了通用型数据类型的概念,使用void表示,许多系统调用都使用这个类型。

库函数执行出错经常返回一个空值,系统调用返回为-1.系统调用执行错误时,把一个错误代号赋给一个静态变量(全局变量)errno。错误代号还关联一段文本描述—可以知道错误原因。

某些关键系统调用具有原子性--多个进程在竞争同一个资源的时候,需要注意处理从而避免发生死锁。

打开文件:int open(const char* path,intoflag,...)返回文件描述符,path可以是绝对路径或者是相对路径的,oflag参数表明文件打开的模式,定义在/usr/include/fcntl.h文件中,所用到的模式如下:

O_RDONLY打开文件用于读操作

O_WRONLY打开文件用于写操作

O_RDWR打开文件用于读写操作

O_APPEND按追加模式打开文件(文件打开的模式只能是O_WRONLY|O_RDWR)

O_CREAT若文件不存在,创建文件

O_EXCL若文件存在且使用O_CREAT模式,返回一个错误代号

O_SYNC读写操作同步进行,仅在数据写入到磁盘之后,才允许write返回

文件权限的符号常量定义在sys/stat.h文件中,可以通过cat  /usr/include/linux/stat.h命令来查看。

fd =open(“foo.txt”,O_WRONLY|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); //Permissions are 0644

shell遇到>符文件篇号时,使用这种方式创建一个文件,或者覆盖一个已经存在的文件。标志符和打开模式经过按位|操作,使用符号常量提高程序的可读性。

状态O_EXCL提供了一中安全的措施,防止已存在的文件被覆盖。这个标识可以用来使得两个进程和谐共享一个文件,使用文件锁轮询的方式。

5.3 I/O系统调用

标准库为文件的读操作(fread,fgets,fgetc,scanf,etc..)和文件的写操作(fwrite,fputs,fputc,printf)提供了很多的函数,但实际执行读写操作的只有两个系统调用,readwrite.这两个调用的用法相同,她们进行读写操作时都需要一个用户自定义缓冲区,缓冲区的大小影响读写操作的效率。

5.3.1read调用---读取文件

ssize_t read(int fildes,void * buf,size_t nbyte);

fildes-是文件描述符,nbyte是缓冲区大小,如果想要读取字符,就把buf声明为char类型的。read调用的返回值表示实际读取的字符数(可能会小于实际请求的数据),返回为0表示没有读取任何数据,-1表示函数执行出错。采用下面的循环的方式来遍历文件。

int fd,n;
char buf;
while((n =read(fd,&buf,1)) >0)

write调用:ssize_t write(int fildes,const void* buf,size_t nbytes)

如果文件描述符有错或者底层的设备驱动程序对数据快长度比较敏感,该返回值可能会小于nbytes,如果返回值为0,表示没有写入任何数据,如果是-1,就表示write系统调用中出错了。

5.3.2open调用

系统调用的头文件可以通过man 2 open 来查看使用帮助,很多的系统调用读可以使用man 2 xxx 来查看使用帮助。

int open(const char *path,int oflags);
int open(const char *path,int oflags,mode_t mode);

调用成功时返回为新的文件描述符,失败时返回-1,并设置全局变量errno以指明失败的原因。

5.3.3 一个实例ccp.c --- 文件复制程序

/* Program:ccp.c -- Copies a file with the read and write system calls */
#include<fcntl.h> /*For O_RDONLY,O_WRONLY,O_CREAT etc. */
#include<sys/stat.h> /*For S_IRUSR,S_IWUSR,S_IRGRP etc*/
#define BUFSIZE 1024

int main(void){
int fd1,fd2; //File descriotors for read and write
int n; //number of characters returned by read
char buf[BUFSIZE]; //buffer size should be handled carefully
/* 函数原型:int open(const char* path,int oflag,...)
* 参数描述:path-文件的路径,oflag-表示文件打开的模式,取值范围:O_RDONLY,
* O_WRONLY,O_RDWR,O_APPEND,O_TRUNC(把文件截断成长度为0,简单理解为写覆盖),
* O_CREAT,O_EXCL,O_SYNC(读写操作同步进行)
* 返回值描述:open调用返回的是一个文件描述符,read,write,close,lseek函数要求
* 以这个数值作为参数.
*/
fd1 = open("/etc/passwd",O_RDONLY);
fd2 = open("passwd.bak",O_WRONLY|O_CREAT|O_TRUNC,
S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH); //Mode 664
while((n = read(fd1,buf,BUFSIZE))>0)
write(fd2,buf,n);
//关闭不需要的文件资源,int close(int fd);
close(fd1);
close(fd2);
return 0;
}
编译命令: cc/gcc -o ccp ccp.c 

执行ccp程序, ./ccp

参看passwd.bak, cat passwd.bak

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
proxy:x:13:13:proxy:/bin:/bin/sh
www-data:x:33:33:www-data:/var/www:/bin/sh
backup:x:34:34:backup:/var/backups:/bin/sh
list:x:38:38:Mailing List Manager:/var/list:/bin/sh
irc:x:39:39:ircd:/var/run/ircd:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
libuuid:x:100:101::/var/lib/libuuid:/bin/sh
syslog:x:101:103::/home/syslog:/bin/false
messagebus:x:102:105::/var/run/dbus:/bin/false
colord:x:103:108:colord colour management daemon,,,:/var/lib/colord:/bin/false
lightdm:x:104:111:Light Display Manager:/var/lib/lightdm:/bin/false
whoopsie:x:105:114::/nonexistent:/bin/false
avahi-autoipd:x:106:117:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false
avahi:x:107:118:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/bin/false
usbmux:x:108:46:usbmux daemon,,,:/home/usbmux:/bin/false
kernoops:x:109:65534:Kernel Oops Tracking Daemon,,,:/:/bin/false
pulse:x:110:119:PulseAudio daemon,,,:/var/run/pulse:/bin/false
rtkit:x:111:122:RealtimeKit,,,:/proc:/bin/false
speech-dispatcher:x:112:29:Speech Dispatcher,,,:/var/run/speech-dispatcher:/bin/sh
hplip:x:113:7:HPLIP system user,,,:/var/run/hplip:/bin/false
saned:x:114:123::/home/saned:/bin/false
xiajian:x:1000:1000:xiajian,,,:/home/xiajian:/bin/bash
dictd:x:115:125:Dictd Server,,,:/var/lib/dictd:/bin/false

备注: 需要注意fopen和open函数的区别,fopen返回的是一个指向FILE结构体的指针,fread和fwrite通过这个指针执行文件的读写,实际上这些库函数也是执行的相应的系统调用(open,read,write)。文件描述符(可以参考百度百科词条)是FILE结构体中的一员。

5.3.4 lseek调用—定位偏移指针

lseek调用可以把文件的偏移指针移动到文件的某个位置,其自身不执行任何的读写操作,只是决定下一次读写的位置。lseek返回值为文件指针相对于文件首的位置,单位是字节。

off_tlseek(int fildes,off_t offset,int whence);

SEEK_SET偏移指针相对与文件首的位置

SEEK_END编移指针相对于文件尾的位置

SEEK_CUR编移位置相对于当前的位置

whence设置为SEEK_END,offset可参数可以取正,偏移指针越出EOF界限,从而形成一个稀疏文件(sparsefile)---也称为带洞的文件,在数据库应用中很有用。

两种方式可以在文件末尾添加数据,第一种方法是:O_APPEND状态标志符打开文件;第二种方法是:先用open,后用lseek(fd,0,SEEK_END).第一种方法是原子操作,推荐使用,第二种方法不是原子操作,不推荐使用。

5.3.5 reverse_read.c --逆续读取文件

/* Program:reverse_read.c --Reads a file in reverse -uses lseek*/
#include<fcntl.h>
#include<unistd.h>

int main(int argc,char ** argv){
char buf;
int size,fd;
fd = open(argv[1],O_RDONLY);
/* 函数原型: off_t lseek(int fildes,off_t offset,int whence);
* 参数描述: fildes-文件描述符,offset和whence两个参数决定了文件偏移指针的位
* 置,whence可以是{SEEK_SET-文件首,SEEK_END-文件尾的位置,SEEK_CUR偏移位置相对于当前的位置}
* 返回值:表示文件指针相对于文件首的位置,单位为字节
*备注:read和write调用可以适用于任何类型的文件,但是lseek只能应用于具有寻址特性的文件
*/
size = lseek(fd,-1,SEEK_END); //Pointer taken to EOF -1 ...
while(size-- >= 0){
read(fd,&buf,1); //Read one character at a time
write(STDOUT_FILENO,&buf,1);//and write it immediately
lseek(fd,-2,SEEK_CUR); //Now move file pointer back by two characters
}
printf("\n");
close(fd);
return 0;
}
编译命令: cc/gcc -o  reverse_read reverse_read.c

为了演示这个程序创建一个test.txt文件: echo  One piece is a big treasure. >> test.txt

执行程序:./reverse_read test.txt

执行结果: .erusaert gib a si eceip enO

5.3.6 truncateftruncate—截断文件

open调用中调用中使用O_TRUNC标志,可以把一个文件的长度截断为0,但是依然需要先打开这个文件.truncateftruncate两个系统调用可以将文件截断到任意长度:

int truncate(const char *path,off_t length);	//需要以文件路径名为参数
int ftruncate(int fildes,off_t length); //需要文件描述符作为参数

这两个系统调用和lseek系统调用相结合可以将文件截断到某个长度,然后用lseek移动到截断点,然后从这个位置开始写入内容。

带缓冲和不带缓冲的读写操作

了解一下磁盘实际读写的过程,readwrite不会直接的访问磁盘,实际上他们是通过对一个缓冲区(缓冲是计算机领域最伟大的技术,其思想渗透在计算机的方方面面)执行读写操作的。缓冲池是由内核分配的,用于读写操作的高速缓存。在读的过程中,如果内核缓冲区为空,则内核指示磁盘读取数据,并把数据读入到缓冲区里。磁盘控制器在读取数据时,read系统调用处于阻塞状态,可能会放弃CPU资源。

为了保证某个时刻只有一个read调用从内核缓冲区中调用所有的数据,内核缓冲区的大小应该设置为read的缓冲区大小。read缓冲区(charbuf[])大小的设置会造成程序读取效率很低。

write调用也使用内核缓冲区,write调用执行后,立即返回,并不需要等待实际写入操作完成。内核在稍后一个合适的时间缓冲区数据写入到磁盘,数据库应用程序通常不能使用这种方式写入磁盘。可以用O_SYNC状态标志打开文件,write调用在内核把缓冲区的内存写入到磁盘之后才返回。

与标准库函数不同,readwrite调用对终端执行读写操作时,采用的是非缓冲区方式。Writeprintf的区别。

注意:内核缓冲区是在操作系统安装时设置的。在不同的操作系统中,大小是不相同的。内核缓冲区的大小与文件系统和机器有关,编写程序设置缓冲区的大小并不是一件容易的事情,并且这样的应用程序移植性并不好,为了弥补这方面的不足,库函数使用FILE结构体中有一个缓冲区,它们在运行时,利用malloc函数自动调整大小

5.4错误处理

错误总是难以避免的,例如:资源不可用,存在干扰信号,I/O操作出错或者参数无效等等。系统调用出错时返回值为-1.为了得到健壮的代码,需要经常检查这个条件。

strerror函数:char*strerror(int errnum);
perror函数:voidperror(const char *s);

5.4.1 strerror 函数代码实例

/*Program:show_errors.c -- User strerror to print all error messages */
#include<stdio.h>
#include<string.h> //strerror函数

int main(void){
int i;
extern int sys_nerr; //Total number of error messages

for(i=0;i<sys_nerr;++i)
//char *strerror(int errnum)
printf("%d: %s\n",i,strerror(i));
printf("Number of errors available: %d\n",sys_nerr);
return 0;
}

5.4.2 perror 函数代码实例 

/*Program: show_errno.c -- Displaying system call errors with perror*/
#include<fcntl.h>

int main(int argc,char ** argv){
int fd;
char* filename = "non_existent_file"; //This file must not exist

fd = open(filename,O_RDONLY);
if(fd == -1)
perror("non_existent_file");
if((fd = open("/etc/shadow",O_RDONLY)) == -1)
perror("shadow");
if((fd = open("show_errno.c",O_WRONLY|O_CREAT|O_EXCL,0644)) == -1)
perror("show_errno.c ");
return 0;
}

与strerror函数不同,perror函数不用错误代码做作为参数 ,只是输出与当前errno变量中的 错误代号所对应的错误信息。c错误代号通常都由常量表示 , 定义在 <sys/errno.h> 文件中。下表是这些常量中的一部分。

符号常量

错误代号

错误信息

EPERM

1

非超级用户

ENOENT

2

文件或者目录不存在

ESRCH

3

进程不存在

EINTR

4

系统调用被中断

EIO

5

读写错误

EACCES

13

无权限访问

EEXIST

17

文件存在

ENOTDIR

20

不是一个目录

EISDIR

21

是一个目录

ENOSPC

28

磁盘没有空间

ESPIPE

29

非法移动文件指针

EROFS

30

只读文件系统

使用错误处理方法,检查系统调用可能出现的错误,编写更为健壮的程序代码。

5.4.3 带错误处理的逆序读取程序reverse_read2.c

/*Program: revese_read2.c -- Reads a file in reverse - uses error handling*/
#include<fcntl.h> //For O_RDONLY
#include<unistd.h> //For STDOUT_FILENO
#include<errno.h> //For ENOENT,errno,etc.
#include<stdio.h>

int main(int argc,char ** argv){
int size,fd;
char buf; /* Single-character buffer*/
char *msg = "Not enough arguments\n";

if(argc!=2){ /* Our own user-defined error message */
write(STDERR_FILENO,msg,strlen(msg));/* Crude form of error */
exit(1); /* handling using write */
}

if((fd = open(argv[1],O_RDONLY)) == -1){
if(errno == ENOENT){ // checking for specific error
fprintf(stderr,"%s\n",strerror(errno)); // perror is better.
exit(2);
} else {
perror(argv[1]); /* Using two library functions perror and exist.*/
exit(3);
}
}
lseek(fd,1,SEEK_END); // Pointer taken to EOF+1 first.
while(lseek(fd,-2,SEEK_CUR)>=0){//and then back by two bytes
if(read(fd,&buf,1) !=1){ //A signal can create error here
perror("read");
exit(4);
}
if(write(STDOUT_FILENO,&buf,1)!=1){ //Disk may run out of space
perror("write");
exit(5);
}
}
printf("\n");
close(fd); //Can have error here too
return 0;
}

5.5文件共享

Unix是一个多道程序系统,多道程序系统允许多个进程同时访问同一个文件。以及父进程创建一个子进程,并允许它的子进程使用父进程的文件描述符。

inode节点只保存文件的静态数据,当文件打开时,需要用到文件的动态属性(文件打开模式-O_RDONLY,文件的状态标志{O_CREAT,O_APPEND},文件的偏移位置)。内核在内存中创建三个数据结构保存一个打开的文件的全部的信息,进程通过这些数据访问文件。

  • 文件描述表–-这个数据结构包含了一个进程的全部文件描述符,这个表每个记录指向一个文件名

  • 文件表– 保存了打开一个文件所需的全部参数,文件偏移位置,它指向vnode表。

  • vnode表– 它包含了一个文件所需要inode信息,可以将其看做是inode节点在内存的近似表示(所以有时称它为内核inode)

文件描述表,每个进程只有一个,但是对于其他的两个表,则不然。同一个文件有多个文件表(多次使用open),或者多个进程访问同一个文件表和vnode表。文件表和vnode表都有一个域用于记录引用该表的次数.下图解释了文件和文件描述表的关系。

Unix原理与应用(第四版)学习笔记2--系统调用之文件篇

5.5.1文件描述表

当进程打开一个文件时,内核就返回一个非负的整数—文件描述符给这个进程。文件描述符与另外一个标志保存在文件描述表里。系统为每个进程建立一张文件描述表,shell的三个标准文件位于这个表的前三个位置(0,1,2)

如果我们关闭了描述符为1的文件,系统把这个描述符分配给下次打开的文件(系统就是使用这个特性实现重定向)。标志符FD_CLOEXEC没有使用在open系统调用中,但是fcntl系统调用则利用它当进程使用exec命令运行另一个程序时是否需要关闭这个描述符。默认情况下,进程执行exec命令时不需要关闭描述符

5.5.2文件表

文件表中没一行有一个指针指向文件表,文件表包含了一个文件所需要的全部信息。

  • 文件打开模式(O_RDONLY)

  • 状态标志(O_CREAT,O_TRUNC,etc)

  • 偏移指针位置,决定了下次读写操作的字节位置

  • 引用计数器,表示引用次表的进程或者系统调用的个数

文件描述符和文件表关系是一对多的关系。

5.5.3 vnode

文件表有一个指向vnode表的指针,vnode是整个文件共享模式的第三个表.vnode表是一个抽象的概念,用一种文件系统无关的方式访问inode节点。

vnode表保存了inode节点的全部的数据,但是有一点它和inode节点不同,vnode是在内存中的,可以称为in-coreinode。内存里只有一个vnode,内核根据vnode表的内容更新磁盘上的inode表。如果两个进程同时打开同一个文件,或者同一个程序两次打开同一个文件,系统在文件描述表里创建两行不同的内容,并建立两个不同的文件表,但是这两个文件都指向同一个vnode表。

与文件表一样,vnode表也有一个引用计数器,它记录指向该表的文件个数。当要删除一个文件时,内核首先检查vnode表的引用计数器的值,以确定文件是否处在打开的状态。如果引用计数不为0,则内核不能删除一个文件,但会释放它的inode节点,可能会删除文件在目录文件中的记录.

5.6目录浏览

int chdir(const char* path);
int fchdir(int fildes);
char* getcwd(char *buf,size_t size);

目录切换程序dir.c

/*Program:dir.c -- Directory navigation with chdir and getcwd */
#include<stdio.h>
#include<stdlib.h>
#define PATH_LENGTH 200
void quit(char *message,int exit_status){
perror(message);
exit(exit_status);
}
int main(int argc,char **argv){
char olddir[PATH_LENGTH +1];
char newdir[PATH_LENGTH +1];

if(getcwd(olddir,PATH_LENGTH) == -1) //Getting current directory
quit("getcwd",1);
printf("present work directory:%s\n",olddir);

if((chdir(argv[1]) == -1)) //Changing to another directory
quit("chdir",2);
printf("change directory:%s\n",argv[1]);

getcwd(newdir,PATH_LENGTH); //Getting current directory
printf("present work directory:%s\n",newdir);
exit(0);
}
编译命令: cc -o dir dir.c

执行命令:./dir /usr/include

执行结果

change directory:/usr/include
present work directory:/usr/include

5.7读取目录文件

目录也是文件,与普通文件一样,目录文件也可以打开,读取和写入。但是目录文件因文件系统的不同而有所不同,甚至随着Unix版本的不同而不同。使用openwrite读取目录文件是很繁琐的一件事,Unix提供了一些库函数来处理目录文件:

DIR*opendir(const char * dirname);	//opens a directory
structdirent *readdir(DIR *dirp); //reads a directory
intclosedir(DIR *dirp); //closes a directory

注意,用户没有权利将数据直接写入目录文件,只有内核才有这个特权。这三个函数对应于普通文件读写操作的open,read,close系统调用,只是目录使用DIR的数据结构。Opendirdirname是目录的路径名,返回DIR结构指针,其他两个函数以DIR数据结构的指针作为参数。

一个目录文件保存了每个文件的inode号和文件名。Readdir函数返回的数据结构structdirent有这两项内容。每次调用readdir,就将目录文件中的下一个记录保存到这个结构体里。POSIX规范并没有规定目录的格式,但是它要求dirent结构体至少要包含两项内容。

struct dirent{

         ino_td_ino; //Inode number

         char d_name[]; //Directory name

}

目录浏览程序lls.c

/*Program: lls.c -- User readdir to populate a dirent structure */
#include<dirent.h>
#include<stdio.h>
#include<stdlib.h>

void quit(char * message,int exit_status){
perror(message);
exit(exit_status);
}

int main(int argc,char **argv){
DIR *dir; //Return by opendir
struct dirent *direntry; //Return by readdir
if(argc<2){
printf("arguement number is incorrect.\n");
exit(1);
}
if((dir = opendir(argv[1])) == NULL) //Directory must exist and have read permission
quit("opendir",2);

while((direntry = readdir(dir)) != NULL)
printf("%10d %s\n",direntry->d_ino,direntry->d_name);

closedir(dir);
return 0;
}
注意:上述程序的逻辑必须存在第二个参数,如果不存在的话就报错,实际上可以做一些修改,如果参数不足两个就输出当前目录下的文件。

#include<dirent.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define LENGTH 80 // 路径字符串的长度
void quit(char * message,int exit_status){
perror(message);
exit(exit_status);
}

int main(int argc,char **argv){
DIR *dir; //Return by opendir
struct dirent *direntry; //Return by readdir
char path[LENGTH+1];
if(argc<2){
getcwd(path,LENGTH);
} else {
strcpy(path,argv[1]);
}
if((dir = opendir(path)) == NULL) //Directory must exist and have read permission
quit("opendir",2);

while((direntry = readdir(dir)) != NULL)
printf("%10d %s\n",direntry->d_ino,direntry->d_name);

closedir(dir);
return 0;
}

编译命令: cc -o lls1 lls1.c

执行命令: ./lls1

命令输出:

8391801 ..
8528499 check_all_perm
8528466 walk_dir.c
8528485 showerrno
8528496 attributes.c
8528497 changetime
8528491 reverse_read2.c
8528481 umask
8529593 test.txt
8528483 error.txt
8528460 .
8528472 readlink.c
8528465 getcwd.c
8528486 lls
8529594 lsdir.c
8528462 .walk_dir.c.swp
8528494 lls.c
8529595 show_errno.c
8528480 lsdir
8528500 check_all_perm.c
8528468 walkdir
8528469 ccp
8528502 faccess.c
8528467 checkfiletype
8528482 ccp.c
8528495 faccess
8528492 attributes
8528487 show_errors.c
8528474 copy_copy.c
8528488 reverse_read2
8528461 lseek
8528464 getcwd
8528473 copy
8528501 changetime.c
8529596 lls1
8528475 copy.c
8528493 dir.c
8528478 passwd.bak
8528489 reverse_read.c
8528484 umask.c
8528470 checkfiletype.c
8528463 lseek.c
8528498 lls1.c
8528471 readlink
8528477 showerrors
8528476 reverse_read
8528479 dir

由此,我发现一个比较好的学习的方法,有时候,就算是自己将书上的代码敲到计算机里,除了练习了打字,并不一定能获得多少深刻的理解。如果自己思考并进行一些小的修改,也许会获得更好的理解。

5.8修改目录文件的记录

虽然不能直接编辑目录文件,可以通过使用函数增加,删除和修改目录文件的记录。

mkdirrmdir---这两个调用具有与其同名的命令相同的功能。link,symlinkunlink---link调用类似ln命令,symlink可以建立符号链接,unlink可以删除目录里的文件记录和符号链接记录。

intmkdir(const char* path,mode_t mode);
intrmdir(const char* path);

mkdir的第二个参数表示新建目录的权限,这个权限要受权限屏蔽字的影响。注意:只有父目录具有写的权限时,才可以在它里面创建目录,

intlink(const char *path1,const char *path2);
intsymlink(const char *path1,const char *path2);

这两个系统调用都会在已存在的目录path1中创建一个名为path2的目录项,symlink为会为path2新建一个inode节点,link并没有建立新的inode,它只是把path1inode连接数增加了1.

unlink调用的作用正好与linksymlink相反,它删除文件的目录项,或者把文件inode的连接数减1。只有当文件连接数减为0,这个文件才会被删除,此时内核释放它的inode节点和相应的磁盘块区用于再次分配,前提是这个文件已经关闭了。

事实:如果一个程序打开了一个文件,内核照样可以删除这个文件在目录文件中的记录,但是只要还有一个文件描述符还指向这个文件,就不能删除这个文件。常见的情况是:刚用open调用打开一个文件,就把unlink命令作用在这个文件上,然后继续从这个文件读取数据。

注意:在文件打开后使用unlink调用删除这个文件,这个方法可以应用在一个有多个出口的程序中.如果用户的应用程序需要从一个临时文件中读取数据,并且要在程序结束时删除这个文件,使用unlink命令断开链接很有效--这个也是Linux程序员常用的技巧之一。

int rename(const char *old,const char *new);

rename系统调用可以重命名任何类型的文件名。与mv命令一样,它可以作用在三类文件上。rename调用只把目录记录中原来的文件名替换成新文件名,当它作用于普通文件或符号文件时,它的作用和mv命令一样,当它作用目录时,他的作用不同于mv在一下的方面:

  • 如果old是一个目录,new是一个已存在的目录,rename调用可以删除new目录,并把old改名为new.mv命令则会把old变成new的一个子目录

  • 如果old是一个普通文件,new不可以是一个目录,mv命令没有这个限制

5.9stat – 读取inode

vnode表的作用是:利用它,可以访问inode节点信息,inode节点包含了全部的文件属性(文件名除外)。要获取这些属性,我们不许访问stat数据结构,这个结构使用通过一个与之同名的系统调用获取。

struct  stat {
ino_t st_ino; //Inode number
mode_t st_mode; //Mode(type and permission
nlink_t st_nlink; //Number of hard links
uid_t st_uid; //UID(owner)
gid_t st_gid; //GID(group owner)
dev_t st_rdev; //Device ID(for device files)
off_t st_size; //File size in bytes
time_t st_atime; //Last access time
time_t st_mtime; //Last modification time
time_t st_ctime; //Last time of change of inode
blksize_t st_blksize; //Preferred block size for I/O
blkcnt_t st_blocks; //Number of blocks allocated
}

ls命令就是根据这个数据结构获得文件属性,根据dirent获得文件名。利用stat系统调用(fstat或者lstat)也可以得到同样的信息。

int stat(const char *path,struct stat *buf);
int fstat(int fildes,struct stat *buf);
int lstat(const char *path,struct stat *buf);

stat系统调用要求第一个参数是文件的路径名,fstat的第一个参数是文件描述符。Stat调用可以跟踪符号链接,例如foo文件是bar文件的一个符号链接(foo->bar),当把stat调用作用在foo上时,实际上是把bar文件的属性装入到了stat结构体。lstat作用于普通文件和目录时,stat效果一样,作用在符号文件时,不跟踪符号链接。

作用于st_mode成员上的S_IFMT屏蔽字;S_IS系类宏判断文件类型。

lsdir.c—只列出目录:ls命令没有只列出目录的可选项,但是利用目录处理函数和stat结构体提供的数据库,我们可以设计一个程序来实现这个功能。使用opendir获得一个指向DIR结构体的指针,然后用readdir返回一个指向dirent结构体的指针,但是在这个程序里,在使用readdir之前,需要使用chdir系统调用切换到需要处理的目录中。

/*Program:lsdir.c -- List only directories - Uses S_IFMT and S_ISDIR macros */
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<dirent.h>
#include<string.h> //strcpy function
#include<stdlib.h>
#define PATH_LENGTH 100
void quit(char *message,int exit_status){
perror(message);
exit(exit_status);
}

int main(int argc,char *argv[]){
DIR *dir;
struct dirent *direntry; //Return by readdir()
struct stat statbuf; //Address of statbuf used by lstat
mode_t file_type,file_perm;
char dirpath[PATH_LENGTH+1];
if(argc<2) //add some control logical to make program more instreseting
getcwd(dirpath,PATH_LENGTH);
else
strcpy(dirpath,argv[1]);
if((dir = opendir(dirpath)) == NULL)
quit("Couldn't open directory",1);
if((chdir(dirpath)) == -1) //notice:chdir系统调用改变了工作目录,这一点对于lstat调用很重要,因为lstat调用仅查找当前目录下的目录
quit("chdir",2);
while((direntry = readdir(dir)) != NULL){ //Read each entry in directory
if(lstat(direntry->d_name,&statbuf) <0){ //d_name must be in current directory
perror("lstat");
continue;
}
if(S_ISDIR(statbuf.st_mode)){ //if file is a directory
file_type = statbuf.st_mode & S_IFMT;
file_perm = statbuf.st_mode & ~S_IFMT;
printf("%o %4o %s\n",file_type,file_perm,direntry->d_name);
}
}
return 0;
}
备注:原来的代码是没有同lls.c中相似,没有对默认没有参数时处理,这里对当前目录的处理是我自己添加的,可以看到和lls1.c中的代码相似。

编译命令: cc -o lsdir lsdir.c

执行命令: ./lsdir  --对当前目录进行处理,由于当前目录中除了.和..这两个特殊目录以外没有其他的特殊目录,所以,就只输出的这两个目录。

~/p/C/T/l/filesystem> ./lsdir
40000  700 ..
40000  700 .

执行命令 ./lsdir  /usr

~/p/C/T/l/filesystem> ./lsdir /usr
40000  755 ..
40000  755 local
40000  755 sbin
40000  755 .
40000  755 bin
40000  755 src
40000  755 games
40000  755 include
40000  755 share
40000  755 lib

5.10查看系统的存取权限

有两个办法可以查看文件的存取权限,通过检查stat.st_mode的每个权限位,但是这个方法并不是总是有实际意义的。当用户运行一个程序时,他需要知道自己是否有权限访问这个程序,而并不需要知道自己属于哪类用户。两种查看权限的方法:

  • stat结构体中的st_mode成员的每一个位进行测试,所有12位都可以使用这种方法进行测试(check_all_perm.c)

  • 利用access系统调用,通过判断运行该程序的用户的真实UID和真实的GID,可以确定他的真实用户的是否可以访问它(faccess.c--见下文5.10.2)

SUID对文件存取的影响 -- 老实说,一直不是很明白SUID到底有什么用???!!!

chmod u+s reverse_read2; chown root reverse_read2

然后reverse_read2就可以读取/etc/shadow  -- 实际上,修改了之后还是需要使用sudo提升权限才能查看/etc/shadow,难道这个是Ubuntu做得特殊保护??

5.10.1 check_all_perm.c---- 查看12个权限位

/*Program: check_all_perm.c -- Checks all 12 permission bits of a file*/
#include<stdio.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
void print_permissions(char* ,struct stat*);
void check_permission(int,int,char*);
int main(int argc,char* argv[]){
int i,fd,perm;
char* filename = argv[1];
struct stat statbuf;
mode_t perm_flag[] = {S_IRUSR,S_IWUSR,S_IXUSR,S_IRGRP,S_IWGRP,S_IXGRP,
S_IROTH,S_IWOTH,S_IXOTH,S_ISUID,S_ISGID,S_ISVTX};
char *msg[] = { "User-readable","User-writable","User-executable",
"Group-readable","Group-writable","Group-executable",
"Others-readable","Others-writable","Others-executable",
"SUID bit set","SGID bit set","Sticky bit set"};
print_permissions(filename,&statbuf);
for(i=0;i<12;++i)
check_permission(perm,perm_flag[i],msg[i]);
}
void print_permissions(char *fname,struct stat* addbuf){
if(lstat(fname,addbuf)<0){
perror("stat");
exit(1);
}else
printf("File:%s Permissions:%o \n",fname,addbuf->st_mode &~S_IFMT);
}
void check_permission(int perm,int flag,char *message){
if(perm & flag)
printf("%s\n",message);
}
编译命令: cc/gcc -o check_all_perm check_all_perm.c

执行命令: 

~/p/C/T/l/filesystem> ./check_all_perm  /usr/include
File:/usr/include Permissions:755 
User-readable
User-writable
User-executable
Group-readable
Group-writable
Others-readable
SUID bit set
SGID bit set
Sticky bit set

备注: 上述程序假设argv[1]存在,并做了一定的容错的处理,在./check_all_perm 时输出的是stat: Bad address,可以考虑,将其修改为输出当前目录的权限位。

5.10.2 access -- 查看文件所有者的权限

int access(const char * path,int amode);
amode 一般是这样的四个值的组合:R_OK-可读,W_OK-可写,X_OK-可执行,F_OK-文件存在

代码实例faccess.c

/*Program: faccess.c -- Determines a file's access rights using the read UID and GID */
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

void quit(char *message,int exit_status){
perror(message);
exit(exit_status);
}

int main(int argc,char** argv){
short count;
for(count =1 ;count<argc;++count){
printf("%s: ",argv[count]);
if(access(argv[count],F_OK) == -1)
quit("File not found",1);
if(access(argv[count],R_OK) == -1)
printf("Not readable ");
if(access(argv[count],W_OK) == -1)
printf("Not writable ");
if(access(argv[count],X_OK) == -1)
printf("Not executable ");

printf("\n");
}
return 0;
}

执行命令--将命令和ls -l 命令进行对比:

~/p/C/T/l/filesystem> ls -l /etc/passwd
-rw-r--r-- 1 root root 1722 Jul 23 01:38 /etc/passwd
~/p/C/T/l/filesystem> ./faccess /etc/passwd
/etc/passwd: Not writable Not executable 

5.11修改文件属性

stat结构体在读取文件属性时比较有用,但是它不能用来设置文件属性.为了修改文件属性,系统调用提供了很多函数,其中一些函数与相应的命令同名

  • linkunlink—建立硬链接,删除硬链接和符号连接。

  • chmodfchmod—修改文件权限

  • chown–修改文件的所有权和组的所有权

  • utime–修改文件的修改时间和访问时间

int chmod(const char *path,mode_t mode);
int fchmod(int fildes,mode_t mode);
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 utime(const char * path,const struct utimbuf * times);
struct utimbuf{
time_t actime; //Last access time
time_t modtime; //Last modification time
}
utime— 修改文件的时间戳,下面展示一个修改时间戳的文件程序---changetime.c

/*Program: changetime.c -- Sets a file's time stamps to those of another file */
#include<sys/stat.h>
#include<fcntl.h>
#include<utime.h>

void quit(char *message,int exit_status){
perror(message);
exit(exit_status);
}

int main(int argc,char** argv){
struct stat statbuf; //To obtain time stamps for an existing file
struct utimbuf timebuf; //To set time stamps for another file

if(lstat(argv[1],&statbuf) == -1)
quit("stat",1);

timebuf.actime = statbuf.st_atime; //setting members of timebuf with values obtained from
timebuf.modtime = statbuf.st_mtime; //statbuf

if(open(argv[2],O_RDWR | O_CREAT,0644) == -1) //used open only to create it
quit("open",2);
close(argv[2]);

if(utime(argv[2],&timebuf) == -1) //set both time stamps for file that was just created
quit("utime",3);
return 0;
}
编译后然后再运行,实际上,通过ls命令查看文件即可看到执行结果。

5.12小结

cat,chmod等命令的实际执行过程是调用相应的系统调用。系统调用(是操作系统能力的全部体现)是内核中的一个例行的子程序,实现特定的功能,需要访问系统的硬件设备。CPU模式:用户-->内核

open,read,write,close,lseek,opendir,readdir,mkdir,rmdir,closedir,getcwd,chdir

所有的系统调用都要把某个整数值赋给全局变量errno,perror或者strerror输出与之想对应的错误信息。

文件描述符,文件表,vnode,inode节点.

stat,lstat,fstat,access,chmod,chown,utime

参考资料

[1] Unix 原理与应用(第四版),印 Sumitabba Das著 ,吴文国译,清华大学出版社, 2008.1