Unix文件IO相关函数

Unix中大多数文件的操作只需要用到五个函数open、read、write、lseek、和close。本章将详细讲解这些函数的用法以及参数详解。

文件描述符

对于内核而言,所有打开的文件都是通过文件描述符进行引用。文件描述符是一个非负整数。当打开或者创建一个文件的时候,内核向进程返回一个文件描述符。当对这个文件进行读写的时候,将这个参数传递给read或write。LINUX用于IO的数据结构一章中讲了STDIN_FILENO、STDOUT_FILENO以及STDERR_FILENO所对应的文件描述符。

为了保证系统资源的合理使用和安全性,Unix系统对于系统和用户能打开的文件描述符的格式都做了一定的限制。通过一下命令我们可以进行查看:

1
2
sysctl -a | grep fs.file-max //查看系统级限制的文件描述符的个数
ulimit -n //查看用户(进程)级别限制的文件描述符的个数,

文件描述符与文件指针的关系

文件描述符:内核会为每一个运行中的进程在进程控制块(pcb)中维护一个打开文件的记录表,每个表项都有一个指针指向打开的文件,文件描述符就是记录表的索引。

文件指针:C语言使用文件指针而不是文件描述符作为文件IO的句柄,文件指针是指向进程的用户空间中的一个FILE结构的数据结构,FILE结构中包括一个IO缓冲区和一个文件描述符,而文件描述符是
文件描述符表的一个索引,从某种意义上可以将文件指针理解为文件句柄的句柄。

1
2
3
4
5
6
7
8
9
10
typedef struct {
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
} FILE;
  • 文件指针相比于文件描述符是高级的接口.
  • 文件指针使用fread()和fwrite()函数进行操作,文件描述符使用write()和read()函数进行操作
  • 文件指针具有缓冲区,是较高级别的IO,读写时具有缓冲,具有错误指示和EOF检测;文件描述符没有
  • 文件指针具有移植性,文件描述符不能移植到除Unix之外的系统。
  • fopen在stdio.h中,open在fcntl.h中
  • fopen是标准C中定义的,而open是posix中定义的。
  • fwrite/fread处理的速度快于read/write,但是在内存方面read/write性能较好。

函数open()和openat()

函数原型

1
2
int open(const char* path, int oflag, .../*mode_t mode*/);
int openat(int fd, const char* path, int oflag, .../*mode_t mode*/)

返回值说明

若文件打开失败返回-1,打开失败原因可以通过errno或者strerror(errno)查看;

若成功将返回最小的未用的文件描述符的值。

参数说明

  • path为要打开的文件的文件路径
  • oflag为文件打开模式.
  • …为可变参数,可以视情况添加

文件打开模式

文件打开模式标识当前进程对打开文件的操作权限。通常用一个或者多个权限的或来表示。权限列表如下:

flag 含义
O_RDONLY 只读权限
O_WRONLY 只写权限
O_RDWR 读写权限
O_EXEC 可执行权限
O_SEARCH 搜索权限(针对目录)
O_APPEND 每次写都追加到文件的末端
O_CLOEXEC 把close_on_exec设置为文件描述符标识
O_CREATE 若文件不存在,则创建它。使用此选项的时候,需要使用第三个参数指定该新文件的访问权限位
O_DIRECTORY 如果path不是目录则出错
O_EXCL 若同时执行了O_CREATE,若文件存在则出错,可以用此选项测试文件是否存在
O_NOCTTY 如果PATH引用的是终端设备,则不将该终端设备作为该进程的控制终端
O_NOFOLLOW 若PATH引用的是符号链接,则出错
O_NONBLOCK 如果path引用的是FIFO,一个块特殊文件或者一个字符特殊文件,则将文本打开操作和后续的IO设置为非阻塞模式
O_SYNC 使每次write等待物理IO完成,包括该write属性引起的文件属性更新需要的IO
O_TRUNC 如果文件存在,且打开模式为只写或者读写,则将文件内容截断为0
O_TTY_INIT 如果打开一个还未打开的终端设备,设置非标准termios参数值,使其符合Single Unix Specification
O_DSYNC 每次write要等待物理IO操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需要等待文件属性被更新
O_RSYNC 使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作完成

文件访问权限mode_t

mode的值表示了对文件的访问权限。这个访问权限与使用shell命令chmod去修改文件的权限的含义相同,文件的权限包括三大类,分别是当前文件对于文件所有者(u),文件所有者所在的组(g)以及其他
用户(o)而言,对这三种角色又分别具有读写可执行的权限。所以这个参数只有在创建文件的时候才用到,用来指定当前创建的文件所对应的权限。

mode 含义
S_IRUSR 用户读
S_IWUSR 用户写
S_IXUSR 用户可执行
S_IRGRP 组读
S_IWGRP 组写
S_IXGRP 组可执行
S_IROTH 其他读
S_IWOTH 其他写
S_IXOTH 其他可执行

需要注意的是,目录的可执行权限以及读权限与文件的相应权限完全不同。目录的可执行权限表示搜索位,即可搜索权限,若目录不具有可执行权限则不能cd进入文件;
目录的读权限是可以查看目录中文件内容的权限,若文件夹不具有读权限,则ls不能显示目录内的内容。

open与openat的区别

open和openat的区别主要在fd上

  • path参数指定的是绝对路径名,在这种情况下,open与openat相同,fd忽略.
  • path参数是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数通过打开相对路径名所在的文件目录获取。即此时fd为打开相对路径所获取的文件描述符。
  • path参数为相对路径,fd参数为AT_FDCWD,此时相对路径为当前目录,作用于open相同。

函数create()

函数原型

1
int create(const char *path, mode_t mode);

返回值说明

若文件创建失败返回-1;
若创建成功返回当前创建文件的文件描述符。

参数说明

参数与open中对应的参数含义相同

函数功能说明

create(path, mode)函数功能为创建新文件,与open(path, O_CREATE|O_TRUNC|O_WRONLY)功能相同。

函数close()

函数原型

1
int close(int fd);

返回值说明

文件关闭成功返回0,关闭失败返回-1.

函数功能介绍

该函数的作用是关闭指定文件描述符的文件,关闭文件时还会释放该进程加在该文件上的所有的记录锁。当一个进程终止时,内核自动关闭它所有打开的文件。很多程序都是利用这一功能而不是close
函数关闭打开的文件。但是对于长期运行的函数,最好还是使用close关闭打开的文件。

lseek()函数

每个打开的文件在文件表项中存在着对应的当前文件的偏移量(current file offset),通常为非负整数。文件的读写操作都是从当前偏移量开始,并在操作后将偏移量增加相应的字节数。当打开文件
模式为O_APPEND时,该文件的offset是文件末尾,除此之外,其他情况都被初始化为0.

函数原型

1
int lseek(int fd, off_t offset, int whence);

返回值说明

成功则返回新的文件的偏移量;
失败则返回-1.

函数功能

使用lseek()函数显式的为一个打开的文件设置偏移量。lseek仅将文件的偏移量记录在内核中,并不引起IO开销。

参数说明

参数offset的解释与whence相关。

  • 若whence为SEEK_SET,则将该文件的偏移量设置为距离当前文件开始处offset字节。
  • 若whence为SEEK_CUR,则将该文件的偏移量设置为距离当前偏移量加offset个字节,此时offset可正可负。
  • 若whence为SEEK_END,则将该文件的偏移量设置为当前文件长度加offser个字节,此时offset可正可负。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <fcntl.h>
#include <stdio.h>
#include <sys/types.h>
int main() {
int fd = open("./data", O_WRONLY | O_CREAT | O_TRUNC);
lseek(fd, 20, SEEK_END);
write(fd, "haha", 4);
//可以用这种方法查看打开文件的当前偏移量。
//这种方法可以用来确定当前文件是否可以设置偏移量
//若文件是管道、FIFO或者网络套接字,则lseek返回-1,并将errno设置为ESPIPE
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
}

注意一般情况下文件的偏移量不能为负值,但是一些特殊的文件允许偏移量为负值,如在FreeBSD上运行的设备/dev/kmem支持负的偏移量。
因为偏移量可能为负值,所以在比较偏移量的时候不能直接判断其是否小于0,而是要判断其是否等于-1.

文件的偏移量允许大于文件的长度,这时会在文件中出现一些空洞,但是是被允许的。文件中的空洞并不在文件中占磁盘块(block)。

read()函数

函数原型

1
2
3
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);

返回值说明

若读取成功,读到文件末尾返回0,未读到文件末尾返回当前读的字节数。
若读取失败,返回-1。

参数说明

fd为要读取文件的文件描述符。buf为读取文件数据缓冲区,nbytes为期待读取的字节数,通常为sizeof(buf)。

注意read函数默认读入多行,遇到换行不会停止读入,直到读到文件末尾,下一次读取返回值为0.

write()函数

函数原型

1
2
3
#include <unistd.h>
ssize_t write(int fd, const void* buf, size_t ntyes);

返回值说明

若写入成功则返回写入的字节数;失败返回-1.

参数说明

buf为写入内容的缓冲区,ntyes为期待写入的字节数,通常为sizeof(buf)。一般情况下返回值与ntypes相等,否则写入失败。

当指定O_APPEND选项,内容将从文件末尾写入,否则从文件开始写入。一般情况下将缓冲区的长度设置为磁盘块的大小可以最大程度的提升程序读写的性能。

Comment and share

Unix错误处理

当Unix系统函数出错的时候,通常会返回一个负值,同时整型变量errno通常被设置为具有特定信息的值。例如当使用open打开文件的时候,若当前文件不存在,此时open的返回值为-1,errno被设置为2(ENOENT)。
系统函数出错的返回值不一定为负数,是根据具体函数具体定义的,如当系统函数返回一个指针时,若出错,将会返回NULL。

Linux系统中errno.h中定义了一系列的错误宏,他们之处了不同错误对应的错误ID,为整型变量,可被赋值。

关于errno需要注意的两点:

  • 如果没出错,errno将不会被进程设置,所以一般当利用函数返回值确认已经出错的时候,再去查看相应的errno
  • 任何函数不会讲errno的值设置为0,而且在errno.h中定义的所有宏定义都不为0

Unix错误处理的两个相关函数

strerror函数

函数原型

1
2
3
#include <string.h>
char* strerror(int errno);

函数功能

此函数的功能为将errno转化为其对应的具体错误信息。

示例代码

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
int fd = open("data");
if (fd == -1) {
printf("%s", strerror(errno));
}
}

perror函数

函数原型

1
2
#include <stdio.h>
void perror(const char* msg);

函数功能

perror基于当前errno的值,在标准错误流上输出一个出错信息。输出内容首先输出msg所指的字符串,然后一个冒号,空格,接着是errno对应的错误字符串,最后是一个换行符。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int fd = open("data");
if (fd == -1) {
printf("%s", strerror(errno));
perror(argv[0]);
}
}

一些特殊的错误

EAGAIN,EWOULDBLOCK和EINTR

在Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。

从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。

  例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返 回,

read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。

  又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。

EAGAIN:Linux - 非阻塞socket编程处理EAGAIN错误

  在linux进行非阻塞的socket接收数据时经常出现Resource temporarily unavailable,errno代码为11(EAGAIN),这是什么意思?

这表明你在非阻塞模式下调用了阻塞操作,在该操作没有完成就返回这个错误,这个错误不会破坏socket的同步,不用管它,下次循环接着recv就可以。

对非阻塞socket而言,EAGAIN不是一种错误。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。

另外,如果出现EINTR即errno为4,错误描述Interrupted system call,操作也应该继续。

最后,如果recv的返回值为0,那表明连接已经断开,我们的接收操作也应该结束。

错误恢复

我们可以将errno.h中定义的错误分为两种,分别是致命的和非致命的,对于致命性错误,无法执行恢复操作。对于非致命性错误,大多数来说是暂时的。

对于非致命性错误,最常用的做法就是延迟一段时间,然后重试。例如当错误表示网络不可用,这时我们将可以通过延迟一段时间,进行重新连接。

errno宏定义及相应解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124 EMEDIUMTYPE Wrong medium type
  123 ENOMEDIUM No medium found
  122 EDQUOT Disk quota exceeded
  121 EREMOTEIO Remote I/O error
  120 EISNAM Is a named type file
  119 ENAVAIL No XENIX semaphores available
  118 ENOTNAM Not a XENIX named type file
  117 EUCLEAN Structure needs cleaning
  116 ESTALE Stale NFS file handle
  115 EINPROGRESS +Operation now in progress
  114 EALREADY Operation already in progress
  113 EHOSTUNREACH No route to host
  112 EHOSTDOWN Host is down
  111 ECONNREFUSED Connection refused
  110 ETIMEDOUT +Connection timed out
  109 ETOOMANYREFS Too many references: cannot splice
  108 ESHUTDOWN Cannot send after transport endpoint shutdown
  107 ENOTCONN Transport endpoint is not connected
  106 EISCONN Transport endpoint is already connected
  105 ENOBUFS No buffer space available
  104 ECONNRESET Connection reset by peer
  103 ECONNABORTED Software caused connection abort
  102 ENETRESET Network dropped connection on reset
  101 ENETUNREACH Network is unreachable
  100 ENETDOWN Network is down
  99 EADDRNOTAVAIL Cannot assign requested address
  98 EADDRINUSE Address already in use
  97 EAFNOSUPPORT Address family not supported by protocol
  96 EPFNOSUPPORT Protocol family not supported
  95 EOPNOTSUPP Operation not supported
  94 ESOCKTNOSUPPORT Socket type not supported
  93 EPROTONOSUPPORT Protocol not supported
  92 ENOPROTOOPT Protocol not available
  91 EPROTOTYPE Protocol wrong type for socket
  90 EMSGSIZE +Message too long
  89 EDESTADDRREQ Destination address required
  88 ENOTSOCK Socket operation on non-socket
  87 EUSERS Too many users
  86 ESTRPIPE Streams pipe error
  85 ERESTART Interrupted system call should be restarted
  84 EILSEQ Invalid or incomplete multibyte or wide character
  83 ELIBEXEC Cannot exec a shared library directly
  82 ELIBMAX Attempting to link in too many shared libraries
  81 ELIBSCN .lib section in a.out corrupted
  80 ELIBBAD Accessing a corrupted shared library
  79 ELIBACC Can not access a needed shared library
  78 EREMCHG Remote address changed
  77 EBADFD File descriptor in bad state
  76 ENOTUNIQ Name not unique on network
  75 EOVERFLOW Value too large for defined data type
  74 EBADMSG +Bad message
  73 EDOTDOT RFS specific error
  72 EMULTIHOP Multihop attempted
  71 EPROTO Protocol error
  70 ECOMM Communication error on send
  69 ESRMNT Srmount error
  68 EADV Advertise error
  67 ENOLINK Link has been severed
  66 EREMOTE Object is remote
  65 ENOPKG Package not installed
  64 ENONET Machine is not on the network
  63 ENOSR Out of streams resources
  62 ETIME Timer expired
  61 ENODATA No data available
  60 ENOSTR Device not a stream
  59 EBFONT Bad font file format
  57 EBADSLT Invalid slot
  56 EBADRQC Invalid request code
  55 ENOANO No anode
  54 EXFULL Exchange full
  53 EBADR Invalid request descriptor
  52 EBADE Invalid exchange
  51 EL2HLT Level 2 halted
  50 ENOCSI No CSI structure available
  49 EUNATCH Protocol driver not attached
  48 ELNRNG Link number out of range
  47 EL3RST Level 3 reset
  46 EL3HLT Level 3 halted
  45 EL2NSYNC Level 2 not synchronized
  44 ECHRNG Channel number out of range
  43 EIDRM Identifier removed
  42 ENOMSG No message of desired type
  40 ELOOP Too many levels of symbolic links
  39 ENOTEMPTY +Directory not empty
  38 ENOSYS +Function not implemented
  37 ENOLCK +No locks available
  36 ENAMETOOLONG +File name too long
  35 EDEADLK +Resource deadlock avoided
  34 ERANGE +Numerical result out of range
  33 EDOM +Numerical argument out of domain
  32 EPIPE +Broken pipe
  31 EMLINK +Too many links
  30 EROFS +Read-only file system
  29 ESPIPE +Illegal seek
  28 ENOSPC +No space left on device
  27 EFBIG +File too large
  26 ETXTBSY Text file busy
  25 ENOTTY +Inappropriate ioctl for device
  24 EMFILE +Too many open files
  23 ENFILE +Too many open files in system
  22 EINVAL +Invalid argument
  21 EISDIR +Is a directory
  20 ENOTDIR +Not a directory
  19 ENODEV +No such device
  18 EXDEV +Invalid cross-device link
  17 EEXIST +File exists
  16 EBUSY +Device or resource busy
  15 ENOTBLK Block device required
  14 EFAULT +Bad address
  13 EACCES +Permission denied
  12 ENOMEM +Cannot allocate memory
  11 EAGAIN +Resource temporarily unavailable
  10 ECHILD +No child processes
  9 EBADF +Bad file descriptor
  8 ENOEXEC +Exec format error
  7 E2BIG +Argument list too long
  6 ENXIO +No such device or address
  5 EIO +Input/output error
  4 EINTR +Interrupted system call
  3 ESRCH +No such process
  2 ENOENT +No such file or directory
  1 EPERM +Operation not permitted
  0 Success

Comment and share

Unix基础知识

Unix体系结构

从严格意义上讲,可以将操作系统定义为一种软件,它相当于一种控制计算机硬件资源,为程序提供运行环境的软件。我们通常将这种软件叫做内核,因为它相对比较小,并且位于环境的核心。Unix体系结构如下图:

内核的接口被称为系统调用(system Call),公共库函数建立在系统调用接口之上,应用程序既可以使用公共函数库,也可以使用系统调用。shell是一个特殊的应用程序,为运行其他应用程序提供了接口。

Unix文件和目录

文件系统

Unix文件系统是文件和目录的一种层次结构,所有文件的起点都是根目录(root),名称为”/“.

目录是一个包含目录项的文件。逻辑上,可以认为每个目录项都包含一个文件,同时还说明该文件属性的信息。文件属性包括文件类型(普通文件还是目录)、文件权限、;链接到改文件的进程数、文件所有者、文件所有者所在的组 文件大小以及文件最后修改的时间等。stat和fstat函数返回一个文件属性的信息结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <sys/stat.h>
//通过文件名获取文件信息,并保存在buf所指的结构体stat中
//返回值:成功返回0,失败-1,错误码存在errno中
int stat(const char* file_name, struct stat* buf);
数据类型:
struct stat {
dev_t st_dev; /* ID of device containing file -文件所在设备的ID*/
ino_t st_ino; /* inode number -inode节点号*/
mode_t st_mode; /* protection -保护模式?*/
nlink_t st_nlink; /* number of hard links -链向此文件的连接数(硬连接)*/
uid_t st_uid; /* user ID of owner -user id*/
gid_t st_gid; /* group ID of owner - group id*/
dev_t st_rdev; /* device ID (if special file) -设备号,针对设备文件*/
off_t st_size; /* total size, in bytes -文件大小,字节为单位*/
blksize_t st_blksize; /* blocksize for filesystem I/O -系统块的大小*/
blkcnt_t st_blocks; /* number of blocks allocated -文件所占块数*/
time_t st_atime; /* time of last access -最近存取时间*/
time_t st_mtime; /* time of last modification -最近修改时间*/
time_t st_ctime; /* time of last status change - */
}

使用C语言实现ls功能

涉及到的相关函数:

  • 文件夹操作函数opendir,readdir,closedir
  • opendir返回指向dir结构的指针,将该指针传递给readdir,无需关心dir结构中存在什么数据,直接读取即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <dirent.h>
#include <stdlib.h>
void err_quit(const char* message) {
printf("%s\n", message);
exit(0);
}
int main(int argc, char *argv[])
{
if (argc < 2)
{
err_quit("usage: ls directory_name\n");
exit(0);
}
DIR *dp;
struct dirent *dirp;
if ((dp = opendir(argv[1])) == NULL)
err_quit("can not open file\n");
while ((dirp = readdir(dp)) != NULL)
printf("%s\n", dirp->d_name);
closedir(dp);
return 0;
}

输入和输出

文件描述符

文件描述符是一个很小的非负整数,内核用文件描述符来标识一个特定进程正在访问的文件。当内核打开或创建文件时,他都会返回一个文件描述符。在读写文件时,都可以使用这个描述符(根据打开时的模式赋予权限).

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

按照惯例,每当运行一个新程序的时候,所有的shell都会为改程序默认打开三个文件描述符,即标准输入,标准输出和标注错误。如果不做特殊处理,这三个描述符都连接到终端,也可以将其重定向到文件。如ls > test.data

Comment and share

函数dup与dup2

函数原型

1
2
3
4
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);

函数功能

这两个函数的功能都是用来复制一个现有的文件描述符。返回的文件描述符与原有的文件描述符共用同一个文件表项,但是文件描述符标志将被清除,即当进程调用exec时文件描述符将不会被关闭。

返回值

dup返回当前可用的最小的文件描述符。dup2返回fd2,若fd2所表示的文件已经打开,则将该文件描述符先关闭,然后再将fd复制到fd2上返回。若fd2与fd相同,则直接返回fd、.

等效函数

dup函数与fcntl的F_DUPFD功能相同。

dup(fd)等效于fcntl(fd, F_DUPFD, 0)

dup2(fd, fd2)等效于close(fd), fcntl(fd, F_DUPFD, fd2)

但是dup2是原子的上述先关闭然后在复制不是原子操作。

作用及用途

dup函数常用来重定向进程的stdin、stdout以及stderr。原理与CGI类似,即将标准输入标准输出重定向到一个文件或者socket流等。

重定向标准输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <stdio.h>
int main() {
int fd = dup(STDIN_FILENO);
if (fd < 0) {
printf("dup error\n");
exit(1);
}
char buf[] = "this is a test of dup\n";
write(fd, buf, sizeof(buf));
return 0;
}

Comment and share

CGI原理

CGI(Common Gateway Interface)通用网关接口,CGI描述了服务器和请求处理程序之间传输数据局的一种标准。在理解的过程中我们需要区分CGI和CGI程序,CGI是一种数据传输的标准,而CGI程序是实际处理业务的一个程序。webserver每请求一次,CGI程序就会fork出一个子进程进行处理。CGI程序的参数通过环境变量和标准输入获得,它的相应通过标准输出传递给webServer。

CGI的工作原理是:

  • 客户端通过http将请求发送到web服务器
  • web服务器接收并收集用户请求,然后交给CGI程序进行处理
  • CGI程序把处理后的结果发送给服务器
  • 服务器将结果传送给浏览器

其中上述第二步web服务器通过环境变量或标准输入将请求发送给CGI程序,第三步CGI通过标准输出将结果发送给webServer

CGI接口标准:标准输入,环境变量以及标准输出

借口标准 介绍
标准输入 CGI程序像其他可执行程序一样,可通过标准输入(stdin)从Web服务器得到输入信息,如Form中的数据,这就是所谓的向CGI程序传递数据的POST方法。这意味着在操作系统命令行状态可执行CGI程序,对CGI程序进行调试。POST方法是常用的方法。
环境变量 操作系统提供了许多环境变量,它们定义了程序的执行环境,应用程序可以存取它们。Web服务器和CGI接口又另外设置了自己的一些环境变量,用来向CGI程序传递一些重要的参数。CGI的GET方法还通过环境变量QUERY-STRING向CGI程序传递Form中的数据。
标准输出 CGI程序通过标准输出(stdout)将输出信息传送给Web服务器。传送给Web服务器的信息可以用各种格式,通常是以纯文本或者HTML文本的形式,这样我们就可以在命令行状态调试CGI程序,并且得到它们的输出。

###常用的环境变量
Linux中的环境变量是一系列的键值对集合,它们的值可以通过shell设置,也可以被其他进程或程序设置和访问,它们是web服务器传递给CGI程序的一种最简单的方式,之所以叫做环境变量是因为它们是全局变量,任何程序都可以存取它们。

key 意义
SERVER_NAME CGI脚本运行时的主机名和IP地址
SERVER_SOFTWARE 你的服务器的类型如: CERN/3.0 或 NCSA/1.3.
GATEWAY_INTERFACE 运行的CGI版本. 对于UNIX服务器, 这是CGI/1.1
SERVER_PROTOCOL 服务器运行的HTTP协议. 这里当是HTTP/1.0.
SERVER_PORT 服务器运行的TCP口,通常Web服务器是80.
REQUEST_METHOD POST 或 GET, 取决于你的表单是怎样递交的
HTTP_ACCEPT 浏览器能直接接收的Content-types, 可以有HTTP Accept header定义.
HTTP_USER_AGENT 递交表单的浏览器的名称、版本 和其他平台性的附加信息。
HTTP_REFERER 递交表单的文本的 URL,不是所有的浏览器都发出这个信息,不要依赖它
PATH_INFO 附加的路径信息, 由浏览器通过GET方法发出.
PATH_TRANSLATED 在PATH_INFO中系统规定的路径信息.
SCRIPT_NAME 指向这个CGI脚本的路径, 是在URL中显示的(如, /cgi-bin/thescript).
QUERY_STRING 脚本参数或者表单输入项(如果是用GET递交). QUERY_STRING包含URL中问号后面的参数
REMOTE_HOST 递交脚本的主机名,这个值不能被设置.
REMOTE_ADDR 递交脚本的主机IP地址.
REMOTE_USER 递交脚本的用户名. 如果服务器的authentication被激活,这个值可以设置。
REMOTE_IDENT 如果Web服务器是在ident (一种确认用户连接你的协议)运行, 递交表单的系统也在运行ident, 这个变量就含有ident返回值.
CONTENT_TYPE 如果表单是用POST递交, 这个值将是 application/x-www-form-urlencoded. 在上载文件的表单中, content-type 是个 multipart/form-data.
CONTENT_LENGTH 对于用POST递交的表单,标准输入口的字节数.

CGI的工作原理图

Comment and share

Wait函数详解

kill

头文件

sys/types.h
signal.h

函数功能

注意此函数的功能是向指定进程发送信号。而不是杀死某个进程.名字为kill的原因是早期的Unix系统对信号的默认处理方式大部分是终止进程。

函数原型

int kill(pid_t pid, int sig);

返回值:执行成功返回0,执行失败返回-1。

参数说明

pid为进程ID,sig标识要发送的信号。

kill()函数的作用是用来向指定的进程或进程组发送信号。其中pid有一下
几种类型:

  • pid > 0: 发送信号给进程标识为pid的进程
  • pid = 0: 发送信号给当前进程相同进程组的所有进程
  • pid = -1:发送信号给系统内除了1号进程以外的所有进程
  • pid < -1:发送信号给进程组标识为-pid的进程。

当sig=0时没有信号发出,但是系统会执行错误检查,通常会利用sig值为0来检查某个进程是否在执行。

  • 若进程不存在,errno为ESRCH
  • 若errno为EPERM(无权向目标进程发送信号,但是存在)或调用成功,表示进程存在。

Example

相关知识:WIFSIGNALED和WIFEXITED两个宏是用来判断当前程序的子进程的退出方式,是接收到信号异常退出
还是正常调用exit()或return退出。

  • WIFEXITED: 调用exit()或return退出
  • WIFSIGNALED:接收到信号异常退出 此时可以使用WTERMSIG(status)获取其接受信号的内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
main()
{
pid_t pid;
int status;
if(!(pid= fork()))
{
printf("Hi I am child process!\n");
sleep(10);
return;
}
else
{
printf("send signal to child process (%d) \n", pid);
sleep(1);
kill(pid, SIGABRT);
wait(&status);
if(WIFSIGNALED(status))
printf("chile process receive signal %d\n", WTERMSIG(status));
}
}

raise和killpg函数

除了kill函数可以向进程发送信号外,还可以通过raise和killpg函数发送信号。

函数原型

int raise(int sig);
int killpg(pid_t pgrpid, int sig);

函数功能

raise函数的功能是向进程自身发送信号.在单线程程序中,调用raise函数相当于调用kill(getpid(), sig).
对于支持线程的系统,一般将raise实现为:

1
pthread_kill(pthread_self(), sig);

此时信号只发送给当前进程的当前线程。而不影响其他线程。

killpg函数的作用是向同组的所有进程发送信号。相当于:

1
kill(-pgrpid, sig);

函数返回值

raise函数的错误返回值只有一个,即EINVAL,表示信号无效。其他时候返回都为成功。
而killpg的返回值与kill类似。

wait()与waitpid()

头文件

sys/wait.h

函数原型

1
2
pid_t wait(int* status);
pid_t waitpid(pid_t pid, int* status, int options);

函数说明

父进程创建子进程之后,父进程使用wait和waitpid具有监视子进程的运行状态的能力。这两个函数用于等待子进程的状态发生变化回调并且获取状态发生变化的信息,所能获取的状态变化包括:子进程运行结束,子进程被信号量暂停,子进程被信号量回复运行

父进程执行wait函数之后,父进程会被阻塞在此处,如果子进程状态发生变化,则wait函数会立即返回结果;否则wait函数会一直阻塞直到子进程状态发生变化。

通常意义上,,如果子进程状态发生了变化,但是还是未被其父进程或者其他系统回调执行wait函数,此时的子进程被称为可等待的。

子进程运行结束后父进程执行wait函数可以推动系统释放与子进程相关的资源;否则子进程将会被维持在僵尸进程(子进程已结束,而父进程还在运行)的状态下一直存在。

返回值说明

上述函数的返回值有-1,0,>0三种情况。分别对应于以下三种情况返回。

  • -1:调用出错,此时出错信息在errno中
  • 0:若waitpid的options设置了WNOHANG,且调用中没有子进程退出,立即返回0
  • >0:若大于0,返回退出进程的pid。

参数说明

  • pid:要监听的进程的ID(<-1, =-1, =0, > 0)
  • status: 用于存储出发状态变化时的信号值和exit(code)中的code值。
  • options 提供一些额外的选项控制waitpid,目前linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数宏,可以使用|连接使用.

pid详细说明:

  • pid < -1 : 监听范围为进程组为-pid的所有子进程
  • pid = -1 : 监听范围为调用wait/waitpid的所有子进程
  • pid = 0: 监听范围为子进程的进程组ID(与父进程相等)
  • pid > 0: 监听特定pid的进程

status详细说明

status用于保存出发wait的信号值或者退出时exit(code)中的code值

options详细说明

  • WNOHANG : 使用此参数调用waitpid,即使子进程没有退出,他也会立即返回,而不是像wait一直等下去
  • WUNTRACED : 用于调试,极少用

一般情况下使用值为0即可。

wait与waitpid关系

wait实质上是waitpid中pid=-1,options=0时封装,即

wait(&status)与waitpid(-1, &status, 0)完全相同

相关宏

wait.h中定义了一些宏用于解析status的值:

含义
WIFEXITED(status) 子进程正常退出返回true否则false
WEXITSTATUS(status) 当正常退出时,返回exit(code)中的code
WIFSIGNALED 子进程接受信号退出时返回true,否则false
WTERMSIG 被信号量杀死时,返回信号量的值
WIFSTOPED(status) 当子进程被信号量暂停时返回true
WSTOPSIG(status) 被信号量暂停时信号量的值

options值:

常量 含义
WNOHANG 调用wait时制定pid仍未返回,wait立即返回0,用于判断子进程有没有结束
WUNTRACED 当子进程被暂停时,则wait立即返回子进程的pid
WCONTINUED 当被暂停的子进程又被信号量恢复后,则wait立即返回子进程的pid。Linux 2.6.10及以后生效。在Mac 0S X 10.9.5上未生效。

Comment and share

Linux用于I/O的数据结构及fcntl函数详解

Linux内核用于IO的数据结构

内核使用三种数据结构表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响.

  • 进程表项 每个进程在记录表中都有一个记录项,记录项中包含一张打开的文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
    • 文件描述符标志(close_on_exec,close_on_exec是一个进程所有文件描述符的标记位图,每个比特位代表一个打开的文件描述符,用于确定在系统调用execve()时需要关闭的文件句柄)。
    • 指向一个文件表项的指针
  • 文件表项 内核为所有打开文件维护一张文件表(不同进程打开相同文件将有两条记录),每个文件表项中包括:
    • 文件状态标志(read,write,append,async,nonblock等)
    • 当前文件偏移量
    • 指向该文件v(i)节点表项的指针
  • 节点表项。每个打开的文件都有一个v-node结构,v-node中包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,V-node中还包含了文件inode节点信息,这些信息是打开文件时从磁盘上读入内存的,所以文件所有信息都是随时可用的。
    • v节点的信息
    • 当前文件的长度
    • i节点的信息

close_on_exec是一个进程所有文件描述符(文件句柄)的位图标志,每个bit代表一个打开的文件描述符,用于
确定在系统调用execve()时是否需要关闭文件句柄。
当一个进程fork出一个子进程时,通常会在子进程中调用execve()函数
加载执行另一个新程序。此时子进程将完全被新程序替换掉,并在子进程中执行新程序。若一个文件描述符在close_on_exec中对应的
bit被设置,那么在执行execve()时该文件描述符将被关闭,否则该文件描述符将始终处于打开状态。
当打开一个文件的时候,默认情况下文件句柄在子进程中也处于打开状态。
注意文件描述符和文件描述符标志的区别,文件描述符是文件进程打开文件时的文件句柄,文件描述符标志为close_on_exec。

下图显示了一个进程打开两个不同文件时三张表对应的关系:

如果两个独立的进程同时打开同一个文件,三张表之间的对应关系如下:


从上面可以看出,不同进程打开相同的文件时每个进程将获得各自的文件表项,这是因为不同的进程都有各自的文件偏移量。
当我们对文件进行操作的时候,上面三种表项之间的变化关系如下:

  • 当对文件进行写操作时(write),在文件表项中的文件偏移量将增加写入的字节数。如果此时文件偏移量超过了文件长度,更新文件长度为当前的文件偏移量
  • 当用O_APPEND标志打开一个文件,则相应的标志也被设置到文件表项的文件标志状态中。每次对这种具有追加标志的文件进行写操作时,首先将当前文件偏移量设置为文件文件长度,这就使得每次增加的内容都会写到文件末尾。
  • 若使用lseek定位到文件末尾,则文件表项中偏移量被设置为文件长度
  • lseek函数只修改文件表项中的偏移量,不进行任何IO操作。

注意问题:

  • 可能有多个文件描述符指向同一个文件表项,如在fork的时候就有可能发生
  • 注意文件描述符和文件状态标志在作用范围方面的区别。前者只用于一个进程的描述符,而后者则应用于指向该给定文件表项的任何进程中的所有描述符。

函数fcntl功能及用法

函数原型:

1
2
3
#include <fcntl.h>
int fcntl(int fd, int cmd, .../*int args or lock args*/);

函数功能:
fcntl的作用是改变已经打开的文件属性。

参数说明:

  • fd 为file descriptor,即文件打开之后的文件描述符
  • cmd为命令,即需要对fd操作的命令,一般为几个宏定义中的其中一个
  • args 参数,此参数为执行cmd命令所需要的参数

cmd参数命令及功能:

fcntl的功能可以分为5种:

  • 复制一个已有的文件描述符
    • cmd = F_DUPFD,此功能*返回一个文件描述符,新的描述符的值为大于或等于args的可用的(尚未打开)文件描述符的最小值,新描述符与fd共用一个文件表项。但是新的文件描述符有它自己的一套文件描述符标志.
    • cmd = F_DUPFD_CLOEXEC.与上述功能一致,唯一不同的是使用此命令会设置CLOSE_ON_EXEC,
      即当执行execve的时候,文件描述符将被关闭。
  • 获取或设置文件描述符标志
    • cmd = F_GETFD 返回与fd关联的close_on_exec标志,第三个参数被忽略。
    • cnd = F_SETFD 将文件描述符标志close_on_exec设置为第三个参数
  • 获取或设置文件状态标志
    • cmd = F_GETFL 获取fd对应的文件的状态标志(存储于文件表项)
    • cmd = F_SETFL 设置fd对应文件的状态标志
  • 获取或设置异步IO所有权
    • cmd = F_GETOWN 获取当前接受SIGIO和SIGURG信号的进程IO或者进程组ID。
    • cmd = F_SETOWN 设置接受SIGIO和SIGURG信号的进程ID或进程组ID。返回值为正则为进程,返回值为负数即为进程组。
  • 获取或记录锁(cmd=F_GETLK、F_SETLK、F_SETLKW),此处不详解

文件状态标志说明

文件状态标志存储与文件表项中,它用于说明进程对当前文件的可操作权限。文件的
操作权限说明如下图表所示,权限设置可使用|或者&进行设置.

文件状态标志 功能说明
O_RDONLY 只读权限
O_WRONLY 只写权限
O_RDWR 读写权限
O_EXEC 可执行权限
O_SEARCH 只搜索打开权限
O_APPEND 追加写
O_NONBLOCK 非阻塞模式
O_SYNC 等待写完成(数据和属性)
O_DSYNC 等待写完成(仅数据)
O_RSYNC 同步读写
O_FSYNC 等待写完成
O_ASYNC 异步IO

注意O_RDONLY、O_WRONLY、O_RDWR、O_EXEC、O_SEARCH这个五个标志并不各占
一位,一个文件的访问方式只能取这五个里面的一个。因此检查当前文件的是这
五个标志中的哪一个需要使用屏蔽字O_ACCMODE取得当问方式位,在于这五个标志
进行对比。

代码示例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main() {
int test;
//返回结果:test = 10,返回大于等于第三个参数可用的fd(10)。
test = fcntl(STDIN_FILENO, F_DUPFD, 10);
printf("result of fcntl(STDIN_FILENO, F_DUPFD, 10) is:%d\n", test);
//返回结果:test=11,与上一个函数功能相同
//不同之处在于其设置了close_on_exec,当执行exec时关闭响应的文件描述符
//注意有一些版本的系统上没有定义这个宏
//test = fcntl(STDIN_FILENO, F_DUPFD_CLOEXEC, 10);
//返回结果 test = 0. 标准输入的文件描述符标志(即关闭标志)为0,
//意思是执行exec时关联的文件描述符不关闭
test = fcntl(STDIN_FILENO, F_GETFD);
printf("the result of fcntl(STDIN_FILENO, F_GETFD) is:%d\n", test);
//将文件描述符标志设置为0
fcntl(STDIN_FILENO, F_SETFD, 0);
//返回结果 test = 32270
test = fcntl(STDIN_FILENO, F_GETFL);
printf("the result of fcntl(STDIN_FILENO, F_GETFL) is:%d\n", test);
//给STDIN_FILENO对应的IO添加非阻塞权限
test |= O_NONBLOCK;
fcntl(STDIN_FILENO, F_SETFL, test);
test = fcntl(STDIN_FILENO, F_GETFL);
printf("the result of fcntl(STDIN_FILENO, F_GETFL) is:%d\n", test);
}

STDIN_FILENO,STDOUT_FILENO以及STDERR_FILENO

STDIN_FILENO等是系统API接口库中的宏定义,它是一个int类型的值,是打开文件的句柄,
对应的主要函数有open,read,write和close等。
STDIN_FILENO的含义是标准输入(键盘)的文件描述符,STDOUT_FILENO是标准输出流的文件描述符,STDERR_FILENO
是标准错误流的文件描述符。

STDIN_FILENO与stdin的区别

  • 数据类型不同 stdin的数据类型为FILE*,STDIN_NO的数据类型为int
  • 可用的函数不同 stdin主要用的函数有fread,fwrite,fclose,STDIN_FILENO可用的函数为write,read和close
  • stdin属于标准IO,高级的输入输出函数,在stdio.h中定义;STDIN_FILENO是文件描述符,一般定义为0,1,2,属于没有buffer的IO,直接调用系统调用,定义在unistd.h中
  • 层次不同,stdin属于标注库处理的输入流,其声明为FILE*型,对应的函数前面都有f开头;而STDIN_FILENO属于系统API接口,对用的函数是一些系统级的调用

Comment and share

魏传柳(2824759538@qq.com)

author.bio


Tencent


ShenZhen,China