mkdir和rmdir
我们可以使用mkdir和rmdir来创建和移除目录.
其语法如下:
#include <sys/stat.h>
int mkdir(const char *path, mode_t mode);
mkdir系统调用可以用来创建目录,而这是与mkdir程序相等同的.mkdir以path为名字创建一个新的目录.目录的权限是由参数mode来指定的,而这也与open系统调用中的O_CREAT的选项是一样的,而且这也是要受到umask的影响.
rmdir语法如下:
#include <unistd.h>
int rmdir(const char *path);
rmdir系统调用将会删除目录,但是只有这个目录为空时才会操作成功.rmdir程序使用系统调用来完成他的工作.
chdir和getcwd
程序在目录中浏览的方式与用户在文件系统中浏览的方式相类似。与我们在Shell中使用cd命令切换目录相类似,一个程序也可以使用chdir系统调用。
#include <unistd.h>
int chdir(const char *path);
程序可以通过调用getcwd函数来确定当前的工作目录。
#include <unistd.h>
char *getcwd(char *buf, size_t size);
getcwd函数将当前目录的名字写入指定的缓冲区buf中。如果目录的名字超过了作为参数传递的缓冲区的尺寸size(ERANGE错误),则会返回null。如果成功则会返回buf。
浏览目录
Linux 系统上的一个通常问题就是目录浏览,也就是说,决定文件所在的特定的目录。在Shell程序中,这是很简单的,只需要Shell扩展通配符表达式即可。在 过去,不同的Unix变种允许程序员访问底层的文件系统结构。我们仍然可以像打开普通文件一样打开一个目录,然后直接读取目录实体,但是不同的文件系统结 构以及实现使得这种方法不可移植。现在已经开发了一个标准的库函数套件,使得目录浏览更为简单。
目录函数在头文件dirent.h中进行 了声明。他们使用一个结构,DIR,作为目录操作的基础。指向这个结构的指针,称之为一个目录流(DIR *),其作用与通常文件操作的文件流(FILE *)相类似。在dirent结构中返回的目录实体本身也在dirent.h中进行声明,因而我们绝不应直接修改DIR结构的域。
我们将会了解下面这些函数:
opendir,closedir
readdir
telldir
seekdir
opendir
opendir函数会打开一个目录,并且建立一个目录流。如果成功,他会返回一个指向用来读取目录实体的DIR结构。
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
如果失败,则会返回一个null指针。注意,一个目录流使用低层的文件描述符来访问目录本身,所以如果打开太多文件时opendir会失败。
readdir
readdir 函数会返回一个指向目录流dirp中下一个目录实体详细信息的结构。成功调用readdir函数会返回下一个目录实体。如果发生错误,或是到达目录结尾 处,readdir会返回NULL。POSIX系统在返回NULL时并不会改变errno的值,只有当发生错误时才会改变errno的值。
#include <sys/types.h>
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
注意,如果同时有其他的进程正在创建或是删除目录中的文件,readdir浏览并不会列出目录中的所有文件。
dirent结构包含目录实体的详细信息,包括下面一些实体:
ino_t d_ino:文件的i节点
char d_name[]:文件名字
要确定目录中一个文件的更为详细的信息,我们需要调用stat函数,我们在前面已经讨论了这个函数。
telldir
telldir函数返回一个目录流中记录当前位置的值。然后我们可以调用seekdir将目录浏览设置为当前位置。
#include <sys/types.h>
#include <dirent.h>
long int telldir(DIR *dirp);
seekdir
seekdir函数设置dirp指定的目录流的目录实体指针。用来设置位置的loc值应是由前面的telldir函数得到的。
#include <sys/types.h>
#include <dirent.h>
void seekdir(DIR *dirp, long int loc);
closedir
closedir函数关闭一个目录流,并且释放与其相关的资源。如果成功则会返回0,否则返回一个错误。
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
在下一个程序printdir.c中,我们集中了各种文件操作函数来创建一个简单的目录列表。目录中的每一个文件列在其所在行。每一个子目录在其名字后跟一个短划线,而其中的文件空余四个空格显示。
程序可以切换进入子目录,这样他所查找的文件就可以有可用的名字,也就是说,他们可以直接传递给opendir。这个程序在过深的嵌套目录中会失败,因为在打开的目录流数量上存在限制。
当然,我们可以通过命令行参数来指定开始点,从而使得程序变得更为通用。我们可以查看ls或是find的Linux源代码来查看更为通用的目录方法。
一个目录浏览程序
1 我们相关的头文件,以及打印当前目录的printdir函数开始。对于子目录,可以使用depth参数来进行重用。
#include <unistd.h>
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
#include <stdlib.h>
void printdir(char *dir, int depth)
{
DIR *dp;
struct dirent *entry;
struct stat statbuf;
if((dp = opendir(dir)) == NULL) {
fprintf(stderr,”cannot open directory: %s/n”, dir);
return;
}
chdir(dir);
while((entry = readdir(dp)) != NULL) {
lstat(entry->d_name,&statbuf);
if(S_ISDIR(statbuf.st_mode)) {
/* Found a directory, but ignore . and .. */
if(strcmp(“.”,entry->d_name) == 0 ||
strcmp(“..”,entry->d_name) == 0)
continue;
printf(“%*s%s//n”,depth,””,entry->d_name);
/* Recurse at a new indent level */
printdir(entry->d_name,depth+4);
}
else printf(“%*s%s/n”,depth,””,entry->d_name);
}
chdir(“..”);
closedir(dp);
}
2 现在我们开始主函数
int main()
{
printf(“Directory scan of /home:/n”);
printdir(“/home”,0);
printf(“done./n”);
exit(0);
}
程序的处理结果输出如下所示:
$ printdir
Directory scan of /h
neil/
.Xdefaults
.Xmodmap
.Xresources
.bash_history
.bashrc
.kde/
share/
apps/
konqueror/
dirtree/
public_html.desktop
toolbar/
bookmarks.xml
konq_history
kdisplay/
color-schemes/
BLP3/
Gnu_Public_License
chapter04/
argopt.c
args.c
chapter03/
file.out
mmap.c
printdir
done.
工作原理
大 多数的动作位于printdir函数中,所以那将是我们查看的地方。通过使用opendir来确定指定的目录是否存在来进行一些初始的检测, printdir在指定的目录上调用chdir函数。当由readdir返回实体不为空时,程序会进行检测确定其是否为一个目录。如果不是,则使用 depth缩进来打印文件实体。
如果这个实体是一个目录,我们就需要进行一些递归操作。在.与..实体(当前目录与父目录)被忽略以后, printdir函数会调用其自身,并且再次进行同样的处理操作。那么如何跳出这个循环呢?一旦while循环结束,chdir("..")函数调用将返 回目录树,从而以先的列表操作就会继续。调用closedir(dp)可以确保打开目录流的数目不会高于其所需要的数目。
为了简略的介绍一下第四章将会讨论的Linux环境,我们来看一下使得程序更为通用的一个方法。程序有限制,是因为他指定为/home/neil目录。通过下面的对main函数的更改,我们可以将其变为一个更为有用的目录浏览器:
int main(int argc, char* argv[])
{
char *topdir = “.”;
if (argc >= 2)
topdir=argv[1];
printf(“Directory scan of %s/n”,topdir);
printdir(topdir,0);
printf(“done./n”);
exit(0);
}
我们改变了三行代码并且添加了五行,现在他就是一个带有目录名可选参数的通用程序了,其默认为当前目录。我们可以用下面的命令来运行:
$ printdir2 /usr/local | more
输出将会进行分页,从而用户可以在输出之中进行分页浏览。这样,用户就会具有了一个方便小巧的通用目录树浏览器。还可以有一些小的修改,我们可以添加空白使用数据,显示的限制深度,等待。
错误
正 如我们所看到的,这一章所描述的许多系统函数与调用都会因为各种原因而失败。当出现这种情况时,他们通过设置外部变量errno来指示其失败原因。许多不 同的库使用这个变量作为报告问题的标准方式。但是在程序指出问题之后,程序必须立即测试errno变量的值,因为他会被下一个函数调用所覆盖,尽管这个函 数本身并没有失败。
错误值与其相关的意义列在头文件errno.h中。他们包括:
EPERM:操作不允许
ENOENT:没有这个文件或目录
EINTR:系统调用中断
EIO:I/O错误
EBUSY:设备或资源忙
EEXIST:文件存在
EINVAL:参数不可用
EMFILE:太多的打开文件
ENODEV:没有这个设备
EISDIR:是一个目录
ENOTDIR:不是一个目录
当发生错误时,有一对十分有用的函数来报告错误:strerror与perror。
strerror
strerror函数将一个错误号映射为一个描述错误类型的字符串。这对于记录错误条件十分有用。
其语法如下:
#include <string.h>
char *strerror(int errnum);
perror
perror函数也将所报告的errno映射为一个字符串,并且打印到标准错误流。他以一个指定的字符串s(如果不为空)开始,然后是一个冒号与空格。
其语法如下:
#include <stdio.h>
void perror(const char *s);
例如:
perror(“program”);
会在标准错误输出上得到下面的输出:
program: Too many open files
/proc文件系统
在这一章的前面,我们提到过Linux将大多数内容看作文件,并且在文件系统中存在硬件设备文件。/dev文件被用来以一种使用低层系统调用的特殊方式来访问硬件。
控制硬件的软件驱动通常可以以特定的方式进行配置,或者是可以报告信息。例如,一个磁盘控制器可以配置使用特定的DMA模式。一个网卡可以报告其是否是高速,多路连接。
与设备驱动进行通信的程序在过去较为常见。例如,hdparm可以配置一些磁盘参数,而ifconfig可以报告其网络参数。在近些年,更为流行的方式是提供一个更为方便的方法来访问驱动信息,而且事实上,在Linux内核的各种元素中已经包含了这些通信。
Linux提供了一个特殊的文件系统,procfs,通常其以目录/proc的形式出现。他包含许多特殊的文件,可以允许高层访问驱动器与内核信息。程序只有要正确的访问权限就可以读取或是写入这些文件来得到信息或是设置参数。
/proc中出现的文件会因系统的不同而不同,而更多的包含在Linux发行版本中作为更多驱动器与程序支持的procfs文件系统。在这里,我们将会看一些其中比较常见的文件,并且简要的讨论其用法。
在编写这一章所使用的计算机上浏览/proc目录会得到下面的一些实体信息:
1/ 1377/ 1771/ 951/ cpuinfo modules
10/ 1401/ 1777/ 961/ devices mounts@
1007/ 1414/ 1778/ 966/ dma mtrr
1023/ 1457/ 2/ 968/ driver/ net/
1053/ 1460/ 3/ 969/ execdomains partitions
1056/ 1463/ 385/ 970/ fb pci
1059/ 1465/ 388/ 971/ filesystems scsi/
1061/ 1476/ 4/ 974/ fs/ self@
1071/ 1477/ 424/ 975/ ide/ slabinfo
1077/ 1479/ 4775/ 976/ interrupts splash
1079/ 1480/ 4850/ 977/ iomem stat
1080/ 1482/ 496/ 978/ ioports swaps
1082/ 1484/ 5/ 979/ irq/ sys/
1086/ 1486/ 535/ 980/ isapnp sysvipc/
1090/ 1491/ 6/ 982/ kcore tty/
1093/ 1494/ 625/ 983/ kmsg uptime
1095/ 1495/ 7/ 999/ ksyms version
1096/ 1496/ 75/ apm loadavg video/
1100/ 1502/ 8/ asound/ locks
1101/ 1503/ 884/ buddyinfo lvm/
1104/ 1545/ 905/ bus/ mdstat
1118/ 1546/ 917/ cmdline meminfo
1119/ 1770/ 932/ config.gz misc
在许多情况下,这些文件可以被读取并给出状态信息。例如,/proc/cpuinfo会给出处理器的详细信息:
$ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 6
model name : Celeron (Mendocino)
stepping : 0
cpu MHz : 451.028
cache size : 128 KB
fdiv_bug : no
hlt_bug : no
f00f_bug : no
coma_bug : no
fpu : yes
fpu_exception : yes
cpuid level : 2
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 sep mtrr pge mca cmov pat
pse36 mmx fxsr
bogomips : 897.84
相似的,/proc/meminfo与/proc/version会分别给出有关内存使用与内核版本的相关信息:
$ cat /proc/meminfo
total: used: free: shared: buffers: cached:
Mem: 527392768 240873472 286519296 0 8331264 134004736
Swap: 139788288 0 139788288
MemTotal: 515032 kB
MemFree: 279804 kB
MemShared: 0 kB
Buffers: 8136 kB
Cached: 130864 kB
SwapCached: 0 kB
Active: 101208 kB
Inactive: 106056 kB
HighTotal: 0 kB
HighFree: 0 kB
LowTotal: 515032 kB
LowFree: 279804 kB
SwapTotal: 136512 kB
SwapFree: 136512 kB
BigFree: 0 kB
$ cat /proc/version
Linux version 2.4.19-4GB (root@Pentium.suse.de) (gcc version 3.2) #1 Wed Nov 27
00:56:40 UTC 2002
更多的特定内核函数的信息可以在/proc的子目录中查找到。例如,我们可以由/proc/net/sockstat得到网络套接口的使用数据:
$ cat /proc/net/sockstat
sockets: used 246
TCP: inuse 20 orphan 0 tw 0 alloc 22 mem 11
UDP: inuse 3
RAW: inuse 0
FRAG: inuse 0 memory 0
/proc中的一些实体可以进行读写。例如,所有运行程序同时可以打开的文件数目是一个Linux内核参数。当前的值可以由/proc/sys/fs/file-max来读取:
$ cat /proc/sys/fs/file-max
52428
在这里其值为52428。如果我们需要增加这上值,我们可以通过写入同样的文件来做到。如果我们正运行一个特殊的程序套件,那么也许我们就需要这样做,例如使用许多表的数据库系统,这需要同时打开许多文件。
注意:写入/proc文件需要超级访问权限。当写入/proc文件时我们要小心,如果我们写入了不正确的值就会引起严重的问题。
如果我们将系统文件处理的限制增加到60000,我们可以简单的向file-max写入这个新的限制数目:
# echo 60000 >/proc/sys/fs/file-max
现在我们可以重新读取这个文件,我们就会看到新值:
$ cat /proc/sys/fs/file-max
60000
/proc下具有数字名字的子目录用来提供访问关于运行程序的信息。我们将会第11章了解到更多的关于程序如何执行为进程的相关信息。
但是现在,我们只需要注意第一个进程具有唯一的一个标识符:在1到32000之间的一个数值。ps命令提供了当前运行进程的一个列表。例如,在编写这一章时:
neil@beast:~/BLP3/chapter03> ps
PID TTY TIME CMD
1104 pts/1 00:00:00 bash
1503 pts/2 00:00:01 bash
1771 pts/4 00:00:00 bash
4991 pts/2 00:00:01 emacs
4994 pts/2 00:00:00 ps
neil@beast:~/BLP3/chapter03>
在这里我们可以看到一些运行bash shell的终端会话,一个运行Emacs文件编辑器的编辑会话。我们可以通过查看/proc了解更多的关于Emacs会话的详细信息。
在这里Emacs进程的标识为4991,所以我们需要查看/proc/4991来得到更为详细的信息:
$ ls -l /proc/4991
total 0
-r--r--r-- 1 neil users 0 2003-02-09 12:45 cmdline
lrwxrwxrwx 1 neil users 0 2003-02-09 12:45 cwd -> /home/neil/BLP3/chapter03
-r-------- 1 neil users 0 2003-02-09 12:45 environ
lrwxrwxrwx 1 neil users 0 2003-02-09 12:45 exe -> /usr/bin/emacs
dr-x------ 2 neil users 0 2003-02-09 12:45 fd
-rw------- 1 neil users 0 2003-02-09 12:45 mapped_base
-r--r--r-- 1 neil users 0 2003-02-09 12:45 maps
-rw------- 1 neil users 0 2003-02-09 12:45 mem
-r--r--r-- 1 neil users 0 2003-02-09 12:45 mounts
lrwxrwxrwx 1 neil users 0 2003-02-09 12:45 root -> /
-r--r--r-- 1 neil users 0 2003-02-09 12:45 stat
-r--r--r-- 1 neil users 0 2003-02-09 12:45 statm
-r--r--r-- 1 neil users 0 2003-02-09 12:45 status
在这里,我们可以看到各种特殊文件来告诉我们这个进程中所发生的事情。
我 们可以看到程序/usr/bin/emacs正在运行,而其当前工作目录为/home/neil/BLP3/chpter03。也可以读取这个目录中的其 他文件来查看其启动所需要的命令以及其所具有的shell环境。cmdline与environ文件作为一系列的非终端字符串来提供这些信息,所以我们查 看他们时需要小心。我们将会在第四章更为深入的讨论Linux环境。
$ od -c /proc/4991/cmdline
0000000 e m a c s /0 d r a f t 2 . t x t
0000020 /0
0000021
在这里,我们可看到Emacs是由命令行emacs draft2.txt来启动的。
fd 子目录提供了关于进程打开的正使用的文件描述符的信息。这个信息十分有用,可以用来确定一个程序一次打开了多少个文件。每一个打开的文件描述符有一个实 体;其名字与文件描述号相匹配。在我们这个例子中,我们可以看到Emacs打开描述符0,1,2,这正是我们所期望的。这是标准输入,输出,以及错误描述 符。尽管此时他正编辑一个文件,他并没有由这个进程保持打开,所以,并不会显示在这里。
$ ls /proc/4991/fd
0 1 2
高级主题:fcntl与mmap
在这里,我们半会讨论一对我们也许会跳过的主题,因为他们很少被用到。正如我们所说的,我们将其放在这里作为引用,因为他们可以为一些技巧问题提供一些简单的解决办法。
fcntl
fcntl系统调用提供了更多的方法来处理底层文件描述符。
#include <fcntl.h>
int fcntl(int fildes, int cmd);
int fcntl(int fildes, int cmd, long arg);
我们可以使用fcntl系统调用在的打开的文件描述符上执行各种操作,包括复制,获取与设置文件描述符标记,获取与设置文件状态标记,以及管理文件锁。
各种操作是由fcntl.h中定义的命令参数cmd的不同值为选择的。依据于所选择的命令,系统调用需要第三个参数,arg:
fcntl(fildes,F_DUPFD,newfd):这个调用会返回一个新的文件描述符,这个文件描述符的值等于或大于newfd的值。新的文件描述符是描述符fildes的一个拷贝。依据打开的文件数目以及newfd的值,这与dup(fildes)同样高效。
fcntl(fildes,F_GETFD):这个调用会返回在fcntl.h中定义的文件描述符标记。这些包括FD_CLOEXEC,这会确定在成功的调用了系统调用的exec家族中的一个后是否关闭。
fcntl(fildes,F_SETFD,flags):这个调用用来调用文件描述符标记,通常只是FD_CLOEXEC。
fcntl (fildes,F_GETFL)与fcntl(fildes,F_SETFL,flags):这些函数分别用来获取与设置文件状态标记与访问模式。我们 可以通过使用fcntl.h中定义的O_ACCMODE掩码来得到访问模式。其他的标记包括传递给使用O_CREAT的open的第三个参数。注意,我们 不能设置所有的标记。通常而言,我们不能使用fcntl来设置文件权限。
我们可以借助fcntl实现咨询文件锁。查看手册页第2节我们可以得到更多的信息,或是第七章,我们将会那里讨论文件锁。
mmap
Unix提供了一个非常有用的程序可以允许程序共享内容,而一个好消息就是这已经包含在2.0以及以后的Linux内核中。mmap函数可以设置一段两个或是多个程序可以读写的内存。一个程序所优的更改可以为另一个程序看到。
我们可以使用同样的程序来操作文件。我们可以使得一个磁盘文件的实体内容看起像是内存中的一个数组。如果文件由C结构可以描述的记录组成,我们可以使用结构数组访问来更新文件。
这是通过具有特殊访问集合的虚拟内存段的使用来做到的。读取与写入段会引起操作系统读取或是写入磁盘文件的相应部分。
mmap函数创建一个指针指向一个与文件内容相关的内存区域,这个文件内容可以由一个打开的文件描述符来访问。
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);
我们可以通过传递off参数来修改由共享段访问的文件数据的起始处。可以访问的数据量(例如,内存段的长度)是通过len参数来设置的。
我们可以使用addr参数来请求一个特定的内存地址。如果为空,结果指针是自动分配的。这是推荐的用法,否则很难移植;系统的变量地址空间是变化的。
prot参数用来设置内存段的访问权限。这是下面这些常量的按位或:
PROT_READ:段可读
PROT_WRITE:段可写
PROT_EXEC:段可执行
PROT_NONE:段不可访问
flags参数控制程序所做的到段的更改是如何反射的:
MAP_PRIVATE 段是私有的,改变是局部的
MAP_SHARED 段的改变反映到文件
MAP_FIXED 段必须在指定的地址addr
msync函数使得内存段的部分或是全部改变写回(或是读取)所映射的文件:
#include <sys/mman.h>
int msync(void *addr, size_t len, int flags);
要更新的段部分是由传递的起始地址,addr以及长度,len是来决定的。flags参数控制如何执行这些更新。
MS_ASYNC 执行异步写
MS_SYNC 执行同步写
MS_INVALIDATE 由文件读取数据
munmap函数释放内存段。
#include <sys/mman.h>
int munmap(void *addr, size_t len);
下面的程序,mmap_eg.c,显示了一个使用mmap与数组格式访问进行更新的结构文件。2.0以前的Linux并不能完全支持mmap的使用。这个程序可以在Sun Solaris以及其他的系统正确工作。
1 我们由定义ERCORD结构与创建NRECOREDS版本开始,每一个记录他们的数字。这些添加到文件records.dat中。
#include <unistd.h>
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>
typedef struct {
int integer;
char string[24];
} RECORD;
#define NRECORDS (100)
int main()
{
RECORD record, *mapped;
int i, f;
FILE *fp;
fp = fopen(“records.dat”,”w+”);
for(i=0; i<NRECORDS; i++) {
record.integer = i;
sprintf(record.string,”RECORD-%d”,i);
fwrite(&record,sizeof(record),1,fp);
}
fclose(fp);
2 我们现在改变记录43到143的整数值,并且写第43个记录字符串处:
fp = fopen(“records.dat”,”r+”);
fseek(fp,43*sizeof(record),SEEK_SET);
fread(&record,sizeof(record),1,fp);
record.integer = 143;
sprintf(record.string,”RECORD-%d”,record.integer);
fseek(fp,43*sizeof(record),SEEK_SET);
fwrite(&record,sizeof(record),1,fp);
fclose(fp);
3 为了将整数值改变为243(并且更新记录字符串),我们将这些记录映射到内存,并且访问第43个记录,在这里使用内存映射:
f = open(“records.dat”,O_RDWR);
mapped = (RECORD *)mmap(0, NRECORDS*sizeof(record),
PROT_READ|PROT_WRITE, MAP_SHARED, f, 0);
mapped[43].integer = 243;
sprintf(mapped[43].string,”RECORD-%d”,mapped[43].integer);
msync((void *)mapped, NRECORDS*sizeof(record), MS_ASYNC);
munmap((void *)mapped, NRECORDS*sizeof(record));
close(f);
exit(0);
}
在第13章,我们将会看到另一个共享内存程序:System V共享内存。
总结
在这一章,我们了解了Linux如何提供直接访问文件与设备。我们也了解了库函数是如何构建在这些底层的函数上来为程序问题提供灵活的解决方案的。结果,我们可以只用几行代码就可以编写出一个相当强大的目录浏览例程。
我们现在已经了解了关于文件与目录操作的足够知识,可以使用更为结构化的基于文件的解决方案将我们在第2章结束时所编写的CD程序转换为C程序。然而,此时,我们并不能为程序添加新的功能,所以我们会在了解了如何处理屏幕与键盘之后再进行重写,而这将是下两章的内容。