首页 > 代码库 > UNIX再学习 -- 文件I/O

UNIX再学习 -- 文件I/O

在 UNIX/Linux 系统中,一切皆文件,这句话想必都有听过。对于文件的操作几乎适用于所有的设备,这也就看出了文件操作的重要性了。在C语言再学习部分有讲过标准I/O文件操作,参看:C语言再学习 -- 文件 下面我们来讲解下系统文件I/O的。

一、文件描述符

1、文件描述符简介

首先从文件描述符开始讲起。因为,对于内核而言,所有打开的文件都是通过文件描述符引用的。那么文件描述符到底是什么?

文件描述符(file descriptor)通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文件。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,使用 open 或 create 返回的文件描述符标识该文件,将其作为参数传送给 read 或 write。

2、标准输入、标准输出和标准错误

按照惯例,UNIX 系统 shell 把文件描述符 0 与进程的标准输入(stdin)关联,文件描述符 1标准输出(stdout)关联,文件描述符 2标准错误(stderr)关联。这是各种 shell 以及很多应用程序使用的惯例,与 UNIX 内核无关。尽管如此,如果不遵循这种惯例,很多 UNIX 系统应用程序就不能正常工作。

这部分讲 shell 编程重定向时,正好讲到了,参看UNIX再学习 -- shell编程

POSIX 定义了 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 来代替 0、1、2。这三个符号常量的定义位于头文件 <unistd.h>

查看 /usr/include/unistd.h
/* Standard file descriptors.  */
#define STDIN_FILENO    0   /* Standard input.  */
#define STDOUT_FILENO   1   /* Standard output.  */
#define STDERR_FILENO   2   /* Standard error output.  */
文件描述符的有效范围是 0 到 OPEN_MAX。一般来说,每个进程最多可以打开 64 个文件(0 — 63)。对于 FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8 以及 Solaris 10 来说,文件描述符的变化范围几乎是无限的,它只受系统配置的存储器总量、整型的字长以及系统管理员所配置的软限制和硬限制的约束。

OPEN_MAX:每个进程最大打开文件数。查看如下:

查看 man sysconf
PEN_MAX - _SC_OPEN_MAX
              The maximum number of files that a process can have open at any time.  Must not be less than _POSIX_OPEN_MAX (20).
或参看:man sysconf()
#include <unistd.h>
#include <stdio.h>
int main (void)
{
    printf("_SC_ARG_MAX: %ld\n", sysconf(_SC_ARG_MAX));
    printf("_SC_CHILD_MAX: %ld\n", sysconf(_SC_CHILD_MAX));
    printf("_SC_CLK_TCK: %ld\n", sysconf(_SC_CLK_TCK));
    printf("_SC_NGROUPS_MAX: %ld\n", sysconf(_SC_NGROUPS_MAX));
    printf("_SC_OPEN_MAX: %ld\n", sysconf(_SC_OPEN_MAX));
    printf("_SC_JOB_CONTROL: %ld\n", sysconf(_SC_JOB_CONTROL));
    printf("_SC_SAVED_IDS: %ld\n", sysconf(_SC_SAVED_IDS));
    printf("_SC_VERSION: %ld\n", sysconf(_SC_VERSION));
	return 0;
}
输出结果:
_SC_ARG_MAX: 2097152
_SC_CHILD_MAX: 7892
_SC_CLK_TCK: 100
_SC_NGROUPS_MAX: 65536
_SC_OPEN_MAX: 1024
_SC_JOB_CONTROL: 1
_SC_SAVED_IDS: 1
_SC_VERSION: 200809
可得,在 Ubuntu 12.04 下测试结果为:_SC_OPEN_MAX =1024 

而在 /apue.3e/exercises/openmax.c 也是有获取系统的 _SC_OPEN_MAX 的程序,可自行查看。

3、最大文件描述符数

通过上面可知,默认的 Linux 进程的最大打开文件描述符数是 1024。但是在某些情况下这点文件描述符数是远远不够的,可根据需要进行更改。

第一种方法:通过 ulimit 命令修改  

参看:ulimit 命令    
ulimit 用于限制 shell 启动进程所占用的资源,支持以下各种类型的限制:所创建的内核文件的大小、进程数据块的大小、Shell 进程创建文件的大小、内存锁住的大小、常驻内存集的大小、打开文件描述符的数量、分配堆栈的最大大小、CPU 时间、单个用户的最大线程数、Shell 进程所能使用的最大虚拟内存。同时,它支持硬资源和软资源的限制。 
作为临时限制,ulimit 可以作用于通过使用其命令登录的 shell 会话,在会话终止时便结束限制,并不影响于其他 shell 会话。而对于长期的固定限制,ulimit 命令语句又可以被添加到由登录 shell 读取的文件中,作用于特定的 shell 用户。
选项:
选项 [options]	含义	例子
-H	设置硬资源限制,一旦设置不能增加。	      ulimit – Hs 64;限制硬资源,线程栈大小为 64K。
-S	设置软资源限制,设置后可以增加,但是不能超过硬资源设置。	        ulimit – Sn 32;限制软资源,32 个文件描述符。
-a	显示当前所有的 limit 信息。	           ulimit – a;显示当前所有的 limit 信息。
-c	最大的 core 文件的大小, 以 blocks 为单位。      	ulimit – c unlimited; 对生成的 core 文件的大小不进行限制。
-d	进程最大的数据段的大小,以 Kbytes 为单位。     	ulimit -d unlimited;对进程的数据段大小不进行限制。
-f	进程可以创建文件的最大值,以 blocks 为单位。     	ulimit – f 2048;限制进程可以创建的最大文件大小为 2048 blocks。
-l	最大可加锁内存大小,以 Kbytes 为单位。       	ulimit – l 32;限制最大可加锁内存大小为 32 Kbytes。
-m	最大内存大小,以 Kbytes 为单位。       	ulimit – m unlimited;对最大内存不进行限制。
-n	可以打开最大文件描述符的数量。	       ulimit – n 128;限制最大可以使用 128 个文件描述符。
-p	管道缓冲区的大小,以 Kbytes 为单位。	      ulimit – p 512;限制管道缓冲区的大小为 512 Kbytes。
-s	线程栈大小,以 Kbytes 为单位。      	ulimit – s 512;限制线程栈的大小为 512 Kbytes。
-t	最大的 CPU 占用时间,以秒为单位。     	ulimit – t unlimited;对最大的 CPU 占用时间不进行限制。
-u	用户最大可用的进程数。     	ulimit – u 64;限制用户最多可以使用 64 个进程。
-v	进程最大可用的虚拟内存,以 Kbytes 为单位。    	ulimit – v 200000;限制最大可用的虚拟内存为 200000 Kbytes。

英文信息,可 man bash 查看
   ulimit [-HSTabcdefilmnpqrstuvx [limit]]
              Provides  control  over  the resources available to the shell and to processes started by it, on systems that allow such con‐
              trol.  The -H and -S options specify that the hard or soft limit is set for the given  resource.   A  hard  limit  cannot  be
              increased  by a non-root user once it is set; a soft limit may be increased up to the value of the hard limit.  If neither -H
              nor -S is specified, both the soft and hard limits are set.  The value of limit can be a number in the unit specified for the
              resource  or  one  of  the  special values hard, soft, or unlimited, which stand for the current hard limit, the current soft
              limit, and no limit, respectively.  If limit is omitted, the current value of the soft limit  of  the  resource  is  printed,
              unless  the  -H  option  is  given.  When more than one resource is specified, the limit name and unit are printed before the
              value.  Other options are interpreted as follows:
              -a     All current limits are reported
              -b     The maximum socket buffer size
              -c     The maximum size of core files created
              -d     The maximum size of a process‘s data segment
              -e     The maximum scheduling priority ("nice")
              -f     The maximum size of files written by the shell and its children
              -i     The maximum number of pending signals
              -l     The maximum size that may be locked into memory
              -m     The maximum resident set size (many systems do not honor this limit)
              -n     The maximum number of open file descriptors (most systems do not allow this value to be set)
              -p     The pipe size in 512-byte blocks (this may not be set)
              -q     The maximum number of bytes in POSIX message queues
              -r     The maximum real-time scheduling priority
              -s     The maximum stack size
              -t     The maximum amount of cpu time in seconds
              -u     The maximum number of processes available to a single user
              -v     The maximum amount of virtual memory available to the shell and, on some systems, to its children
              -x     The maximum number of file locks
              -T     The maximum number of threads

              If limit is given, it is the new value of the specified resource (the -a option is display only).  If  no  option  is  given,
              then  -f  is  assumed.   Values  are  in  1024-byte  increments, except for -t, which is in seconds, -p, which is in units of
              512-byte blocks, and -T, -b, -n, and -u, which are unscaled values.  The return status is 0 unless an invalid option or argu‐
              ment is supplied, or an error occurs while setting a new limit.
实例:

显示当前最大打开文件描述符数

# ulimit -n
1024

修改当前用户环境下的最大打开文件描述符数 (临时更改)

设置当前用户环境下的最大打开文件描述符数为 65536
# ulimit -HSn 65536

查看:
# ulimit -n
65536

将指令添加到脚本中 (永久更改)

添加到 单用户目录下:/etc/bash.bashrc 或 ~/.bashrc

echo "ulimit -HSn 65536" >> ~/.bashrc
或者
echo "ulimit -HSn 65536" >> /etc/bash.bashrc
我使用的是 root 超级用户登录,而非 non-root 登录的,所以放在 /etc/profile 等针对所有用户的不起作用。

还有网上说的写到 rc.local 我也没有实现。

第二种方法:修改 limits.conf 文件 (永久更改)

/etc/security/limits.conf 文件最后加入如下两行:

* soft nofile 65536
* hard nofile 65536
 其中 * 代表所有用户nofile 是代表最大文件打开数用 non-root 登录,通过 ulimit -n 查看是否生效。

告诉你个不幸的消息,很遗憾我的没有生效。将 * 改为 root 则可以生效,因为我用的不是 non-root 登录的。

查看最大文件描述符数上限

当然,这个最大文件描述符数也是有上限的,比如设置一个很大的数,会提示如下错误:
# ulimit -n 100000000
bash: ulimit: open files: 无法修改 limit 值: 不允许的操作
而最大文件描述符数的上限值是在 /proc/sys/fs/nr_open 设置的,默认为 1048576
你也可以修改它,比如执行:
 echo 2000000 > /proc/sys/fs/nr_open

4、混淆的概念

在我看到的很多文章里,有不少将 /proc/sys/fs/file-max 看作了 最大文件描述符数的上限值,其实是不对的。那么到底 file-max 和 nr_open 有什么区别呢?
查看 linux/Documentation/sysctl/fs.txt,可看到关于 file-max 和 nr_open 的解释
file-max & file-nr:

The kernel allocates file handles dynamically, but as yet it
doesn‘t free them again.

The value in file-max denotes the maximum number of file-
handles that the Linux kernel will allocate. When you get lots
of error messages about running out of file handles, you might
want to increase this limit.

Historically, the three values in file-nr denoted the number of
allocated file handles, the number of allocated but unused file
handles, and the maximum number of file handles. Linux 2.6 always
reports 0 as the number of free file handles -- this is not an
error, it just means that the number of allocated file handles
exactly matches the number of used file handles.

Attempts to allocate more file descriptors than file-max are
reported with printk, look for "VFS: file-max limit <number>
reached".
nr_open:

This denotes the maximum number of file-handles a process can
allocate. Default value is 1024*1024 (1048576) which should be
enough for most machines. Actual limit depends on RLIMIT_NOFILE
resource limit.
翻译一下:
关于 file-max
内核可以动态的分配文件句柄,但到目前为止是不会释放它们的。
file-max 的值表示Linux内核分配的最大文件句柄数如果你看到了很多关于打开文件数已经达到了最大值的错误信息,你可以试着增加该值的限制。
在kernel 2.6之前的版本中,file-nr 中的值由三部分组成,分别为:1.已经分配的文件句柄数,2.已经分配但没有使用的文件句柄数,3.最大文件句柄数。但在 kernel 2.6 版本中第二项的值总为 0,这并不是一个错误,它实际上意味着已经分配的文件句柄无一浪费的都已经被使用了。

关于 nr_open
nr_open 的值表示一个进程可以分配的最大文件句柄数。 默认值为1024 * 1024(1048576),应该足够用于大多数机器。 实际限制取决于RLIMIT_NOFILE资源限制。

(1)概念解析

先说 file_max

前面的翻译,我们已经知道 file-max 的值表示Linux内核分配的最大文件句柄数
让我们再查看一下更多关于它的内容: man proc
 /proc/sys/fs/file-max
              This  file  defines  a system-wide limit on the number of open files for all processes.  (See also setrlimit(2), which can be
              used by a process to set the per-process limit, RLIMIT_NOFILE, on the number of files it may open.)  If you get lots of error
              messages about running out of file handles, try increasing this value:

              echo 100000 > /proc/sys/fs/file-max

              The kernel constant NR_OPEN imposes an upper limit on the value that may be placed in file-max.

              If  you  increase  /proc/sys/fs/file-max,  be  sure  to  increase  /proc/sys/fs/inode-max  to  3-4  times  the  new  value of
              /proc/sys/fs/file-max, or you will run out of inodes.
翻译一下:
该文件定义了所有进程的打开文件数量的系统范围限制。 (另见setrlimit(2),可以是被一个进程用来设置每个进程的限制,RLIMIT_NOFILE,对它可能打开的文件数。)如果你收到很多错误关于运行文件句柄的消息,尝试增加此值。
内核常量 NR_OPEN 对可以放在 file-max中 的值施加上限。如果增加 /proc/sys/fs/file-max,请确保将 /pro /sys/fs/inode-max 增加到新值的 3-4 倍,否则 /proc/sys/fs/file-max,否则将用尽inode。
比 file-max 中的值大 3-4 倍,因为 stdin,stdout 和网络套接字也需要一个 inode 来处理它们。

意思是,修改file-max时也要按其值的 3-4 倍来修改 inode-max。
而file-max 的值我们可以使用 cat 查看:
# cat /proc/sys/fs/file-max
100987
而有种说法是 file-max 一般为内存大小(KB)的 10% 来计算,如果使用 shell,可以这样计算:
# grep MemTotal /proc/meminfo | awk ‘{printf("%d\n",$2/10)}‘
102479
经比较,可能由于有各种其他原因导致 file-max 没有设置为内存的 10%。

再有关于 file-nr 中的值由三部分组成,分别为:1.已经分配的文件句柄数,2.已经分配但没有使用的文件句柄数,3.最大文件句柄数。第二项的值总为 0,这并不是一个错误,它实际上意味着已经分配的文件句柄无一浪费的都已经被使用了。
# cat /proc/sys/fs/file-nr
6464	0	100987

而,file-max 的值也有两种方式修改
第一种:临时更改
echo 1000000 > /proc/sys/fs/file-max
第二种:永久更改
修改 /etc/sysctl.conf 文件,末尾增加 fs.file-max = 1000000

注意几句话,
file-max 的值表示Linux内核分配的最大文件句柄数
file-max 该文件定义了所有进程的打开文件数量的系统范围限制。(系统级)
ulimit 用于限制 shell 启动进程所占用的资源。 (进程级)
nr_open 的值表示一个进程可以分配的最大文件句柄数。 (进程级)
这也间接说明了,nr_open 是 ulimit -HSn 的上限值

(2)系统级 和 进程级 的区别

首先,系统级

我们刚才有查看 file-nr:
# cat /proc/sys/fs/file-nr
6464	0	100987
file-nr 中的值由三部分组成,分别为:1.已经分配的文件句柄数,2.已经分配但没有使用的文件句柄数,3.最大文件句柄数。也就是说,系统文件描述符的数量目前被使用 6464.
例如,查看 proc 目录
技术分享

使用 ps 指令查看当前终端启动的进程
使用 ps -aux 表示显示所有包含其他使用者的进程
使用 ps -aux | more 表示将ps -aux的结果进行分屏显示
# ps
  PID TTY          TIME CMD
 2401 pts/1    00:00:00 bash
 3054 pts/1    00:00:00 ps
# ps -aux | more
Warning: bad ps syntax, perhaps a bogus ‘-‘? See http://procps.sf.net/faq.html
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1   3612  2024 ?        Ss   10:53   0:01 /sbin/init
root         2  0.0  0.0      0     0 ?        S    10:53   0:00 [kthreadd]
root         3  0.0  0.0      0     0 ?        S    10:53   0:00 [ksoftirqd/0]
root         6  0.0  0.0      0     0 ?        S    10:53   0:00 [migration/0]
root         7  0.0  0.0      0     0 ?        S    10:53   0:01 [watchdog/0]
root         8  0.0  0.0      0     0 ?        S<   10:53   0:00 [cpuset]
root         9  0.0  0.0      0     0 ?        S<   10:53   0:00 [khelper]
root        10  0.0  0.0      0     0 ?        S    10:53   0:00 [kdevtmpfs]
.....
那我们就看看当前终端启动的进程 2401
lsof 命令可以查出某个进程打开的文件数目,wc -l 只显示列数
lsof使用,参看:lsof 命令
# lsof | grep ‘2401‘ | wc -l
20

意思是有20个文件被打开
查询进程使用的文件描述符数目
# ls -l /proc/2401/fd/ | wc -l
5

意思是文件描述符只有5个
所以说,一个文件即使被打开,也可能没有文件描述符,比如当前工作目录,内存映射文件和可执行文本文件。
以上的 fd 就是系统文件描述符。

再者,进程级

比如,使用open 和 close 函数查看 fd 的值:
//open函数和close函数的使用
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
	
//	while (1){
		int fd = open("a.txt",O_WRONLY|O_CREAT|O_TRUNC,0644);
		if(-1 == fd)
		{
			perror("open"),exit(-1);
		}


		printf ("fd = %d\n", fd);
#if 1		
		//2.关闭文件
		int res = close(fd);
		if(-1 == res)
		{
			perror("close"),exit(-1);
		}

#endif 
//	}
	return 0;
}
输出结果:
fd = 3
该程序,除去标准输入(0)、标准输出(1)、标准错误(2),文件描述符从 3 开始
将上面的程序改为循环,其结果为:
忽略....
fd = 1019
fd = 1020
fd = 1021
fd = 1022
fd = 1023
open: Too many open files
可以看到当 fd = 1023 再往后,出现错误 open: Too many open files,而这个 1024,不就是在 Ubuntu 12.04 下测试结果为:_SC_OPEN_MAX =1024 即:
# ulimit -n
1024
这里的 fd 是进程级的文件描述符。

5、文件描述符和文件指针

(1)文件描述符和文件指针比较

参看:文件句柄(file handles) & 文件描述符(file descriptors)
文件描述符(fd):在linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。
文件指针(FILE*):C 语言中使用文件指针做为 I/O的句柄。文件指针指向进程用户区中的一个被称为 FILE 结构的数据结构。FILE 结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。

通过 /usr/include/libio.h 查看 C 语言中 _IO_FILE 结构体的定义: 
struct _IO_FILE {
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;	/* Current read pointer */
  char* _IO_read_end;	/* End of get area. */
  char* _IO_read_base;	/* Start of putback+get area. */
  char* _IO_write_base;	/* Start of put area. */
  char* _IO_write_ptr;	/* Current put pointer. */
  char* _IO_write_end;	/* End of put area. */
  char* _IO_buf_base;	/* Start of reserve area. */
  char* _IO_buf_end;	/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it‘s too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
我们可以通过 /usr/include/stdio.h  C  语言中 FILE 结构体的定义:
/* Define outside of namespace so the C++ is happy.  */
struct _IO_FILE;

__BEGIN_NAMESPACE_STD
/* The opaque type of streams.  This is the definition used elsewhere.  */
typedef struct _IO_FILE FILE;
__END_NAMESPACE_STD
#if defined __USE_LARGEFILE64 || defined __USE_SVID || defined __USE_POSIX     || defined __USE_BSD || defined __USE_ISOC99 || defined __USE_XOPEN     || defined __USE_POSIX2
__USING_NAMESPACE_STD(FILE)
#endif

# define __FILE_defined	1
#endif /* FILE not defined.  */
#undef	__need_FILE


#if !defined ____FILE_defined && defined __need___FILE

/* The opaque type of streams.  This is the definition used elsewhere.  */
typedef struct _IO_FILE __FILE;
在这个_IO_FILE结构体中的“int _fileno”就是fd,即文件描述符。举例验证:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>

int main(){
        char buf[50] = {"file descriptor demo"};
        FILE *myfile;

        myfile = fopen("test","w+");
        if(!myfile){
                printf("error: openfile failed!\n");
        }
        printf("The openfile‘s descriptor is %d\n", myfile->_fileno);
        if(write(myfile->_fileno,buf,50)<0){
                perror("error: write file failed!\n");
                exit(1);
        }else{
                printf("writefile successed!\n");
        }

        exit(0);
}
输出结果:
The openfile‘s descriptor is 3
writefile successed!

查看 test
cat test 
file descriptor demoroot

(2)文件描述符和文件指针相互转换

文件指针转文件描述符:

参看:百度百科--fileno
函数 fileno
int _fileno( FILE *stream );
 fileno 用来取得参数stream指定的文件流所使用的文件描述符。
实例:
#include <stdio.h>
int main( void )
{
	printf( "The file descriptor for stdin is %d\n", fileno( stdin ) );
	printf( "The file descriptor for stdout is %d\n", fileno( stdout ) );
	printf( "The file descriptor for stderr is %d\n", fileno( stderr ) );
	return 0;
}
输出结果:
The file descriptor for stdin is 0
The file descriptor for stdout is 1
The file descriptor for stderr is 2
#include <stdio.h>
int main(void)
{
	FILE *fp;
	int fd;
	fp = fopen("/etc/passwd", "r");
	fd = fileno(fp);
	printf("fd = %d\n", fd);
	fclose(fp);
	return 0;
}
输出结果:
fd = 3

文件描述符转文件指针:

参看:百度百科--fdopen
函数 fdopen
FILE* fdopen(int fd, const char* type);
fdopen 取一个现存的文件描述符,并使一个标准的I / O流与该描述符相结合。此函数常用于由创建管道和网络通信通道函数获得的描述符。因为这些特殊类型的文件不能用标准I/O fopen函数打开,首先必须先调用设备专用函数以获得一个文件描述符,然后用fdopen使一个标准I/O流与该描述符相结合。
实例:标准输出文件描述符为1
#include<stdio.h>
int main (void)
{
	FILE * fp = fdopen (1, "w+");
	fprintf (fp, "%s\n", "hello!");
	fclose (fp);
	return 0;
}
输出结果:
hello!

UNIX再学习 -- 文件I/O