本文主要介紹了如下內(nèi)容:
- C標(biāo)準(zhǔn)庫(kù)函數(shù)與系統(tǒng)函數(shù)的關(guān)系
- 進(jìn)程控制塊
- 文件描述符
- 系統(tǒng)調(diào)用:open、close、read、write、lseek、fcntl和ioctl
先導(dǎo)概念
C標(biāo)準(zhǔn)庫(kù)函數(shù)與系統(tǒng)函數(shù)的關(guān)系
API層次如圖所示:
API調(diào)用順序
由上往下(用戶態(tài) -> 內(nèi)核態(tài))的順序依次是:
- C標(biāo)準(zhǔn)庫(kù)函數(shù):調(diào)用系統(tǒng)庫(kù)函數(shù)(即 系統(tǒng)調(diào)用);
- 系統(tǒng)調(diào)用:即操作系統(tǒng)的應(yīng)用層API,調(diào)用內(nèi)核層API;
- 內(nèi)核層API: 調(diào)用具體的驅(qū)動(dòng)層API(在Linux中一般以
sys_
開(kāi)頭); - 驅(qū)動(dòng)層函數(shù):直接控制硬件設(shè)備。
以調(diào)用fwrite()函數(shù)將文件內(nèi)容顯示在終端為例,fwrite()函數(shù)將調(diào)用write()系統(tǒng)調(diào)用,而write()系統(tǒng)調(diào)用的實(shí)現(xiàn)則是調(diào)用內(nèi)核態(tài)的sys_write()函數(shù),由sys_write()來(lái)判斷具體調(diào)用哪個(gè)驅(qū)動(dòng)函數(shù)來(lái)訪問(wèn)硬件設(shè)備。
當(dāng)然,對(duì)于Linux操作系統(tǒng)而言,還多了一層VFS(virtual File System,虛擬文件系統(tǒng))層:
write()系統(tǒng)調(diào)用將來(lái)自用戶空間的數(shù)據(jù)流,首先通過(guò)VFS的通用系統(tǒng)調(diào)用,然后通過(guò)文件系統(tǒng)的特殊寫(xiě)法,最后寫(xiě)入物理介質(zhì)中。
各API在緩沖區(qū)上的不同之處
- fopen():每打開(kāi)一個(gè)文件,都會(huì)對(duì)應(yīng)一個(gè)單獨(dú)的緩沖區(qū);
- open():無(wú)緩沖區(qū);
- sys_open:有緩沖區(qū),但是由所有打開(kāi)的文件共用。
關(guān)于緩沖區(qū)的刷新方式:
- 刷新C標(biāo)準(zhǔn)緩沖區(qū)
- 緩沖區(qū)滿,自動(dòng)刷新;
- 手動(dòng)調(diào)用fflush()函數(shù)刷新;
- 使用fclose()函數(shù)關(guān)閉文件時(shí)刷新;
- 程序正常結(jié)束后緩沖區(qū)自動(dòng)刷新。
- 刷新內(nèi)核緩沖區(qū)
由一個(gè)守護(hù)進(jìn)程定時(shí)刷新。
PCB和文件描述符fd
PCB(process control block,進(jìn)程控制塊)在Linux源碼中的實(shí)現(xiàn)即task_struct
結(jié)構(gòu)體,位于/include/linux/sched.h
文件中。該結(jié)構(gòu)體在Linux中被稱為進(jìn)程描述符(process descriptor)。 其部分結(jié)構(gòu)如下(linux kernel 版本為4.4.36):
1380 struct task_struct {
1381 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
1382 void *stack;
1383 atomic_t usage;
1384 unsigned int flags; /* per process flags, defined below */
1385 unsigned int ptrace;
1386
1387 #ifdef CONFIG_SMP
1388 struct llist_node wake_entry;
1389 int on_cpu;
1390 unsigned int wakee_flips;
1391 unsigned long wakee_flip_decay_ts;
1392 struct task_struct *last_wakee;
1393
1394 int wake_cpu;
1395 #endif
1396 int on_rq;
1483 pid_t pid;
1484 pid_t tgid;
Linux內(nèi)核把進(jìn)程的列表存放在任務(wù)隊(duì)列(task list)中,該隊(duì)列是一個(gè)雙向循環(huán)鏈表,鏈表中的每一項(xiàng)都是一個(gè)task_struct
結(jié)構(gòu)體。
在Linux內(nèi)核中,每一個(gè)進(jìn)程都有一個(gè)PCB來(lái)管理,每一個(gè)PCB中都有一個(gè)指向files_struct
結(jié)構(gòu)體的指針:
1564 /* open file information */
1565 struct files_struct *files;
可以看到,task_struct
結(jié)構(gòu)體中的files
是個(gè)指針(充當(dāng)目錄項(xiàng)的角色),指向files_struct
結(jié)構(gòu)體。而files_struct
結(jié)構(gòu)體是一張文件描述符表(實(shí)際上就是一個(gè)整形數(shù)組,里面存放的是諸如0
、 1
、 2
這樣的文件描述符,文件描述符即一些非負(fù)整數(shù)),這些文件描述符指向真正的設(shè)備文件,包括磁盤文件、顯示屏文件等所有文件。
文件描述符struct files_struct
源碼:
(位于linux-4.4.36/include/linux/fdtable.h
中)
43 /*
44 * Open file table structure
45 */
46 struct files_struct {
47 /*
48 * read mostly part
49 */
50 atomic_t count; /* 該結(jié)構(gòu)體的引用計(jì)數(shù) */
51 bool resize_in_progress;
52 wait_queue_head_t resize_wait;
53
54 struct fdtable __rcu *fdt;
55 struct fdtable fdtab;
56 /*
57 * written part on a separate cache line in SMP
58 */
59 spinlock_t file_lock ____cacheline_aligned_in_smp;
60 int next_fd;
61 unsigned long close_on_exec_init[1];
62 unsigned long open_fds_init[1];
63 unsigned long full_fds_bits_init[1];
64 struct file __rcu * fd_array[NR_OPEN_DEFAULT]; /* 缺省的文件對(duì)象數(shù)組 */
65 };
關(guān)系圖:
文件‘開(kāi)’ ‘關(guān)’ ‘讀’ ‘寫(xiě)’的系統(tǒng)接口
open()
功能:打開(kāi)或者創(chuàng)建(如果文件不存在)一個(gè)文件。
每打開(kāi)一個(gè)文件,操作系統(tǒng)內(nèi)核(kernel)就會(huì)在內(nèi)存中新建一個(gè)
files_struct
結(jié)構(gòu)體。在同一個(gè)進(jìn)程中 多次打開(kāi)同一個(gè)文件,內(nèi)核也會(huì)在內(nèi)存中分別新建不同的
files_struct
結(jié)構(gòu)體(由不同的文件描述符映射)。因此,每次打開(kāi)的文件在使用完之后一定要及時(shí)關(guān)閉,否則可能會(huì)引起內(nèi)存泄漏。
聲明:
NAME
open - open and possibly create a file
SYNOPSIS
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
返回值:
- 成功:返回新分配的文件描述符;
- 出錯(cuò):則返回-1,并設(shè)置errno;
close()
功能:關(guān)閉一個(gè)打開(kāi)的文件,一般與open()成對(duì)使用。
每調(diào)用一次
close(fd)
,實(shí)際上是將該文件描述符fd所指向的files_struct
結(jié)構(gòu)體中的引用計(jì)數(shù)count
值減一。當(dāng)引用計(jì)數(shù)值減為0時(shí),操作系統(tǒng)內(nèi)核(kernel)才真正關(guān)閉該文件。通過(guò)調(diào)用dup/dup2系統(tǒng)調(diào)用可使
files_struct
結(jié)構(gòu)體中的引用計(jì)數(shù)count
值加一。具體是dup/dup2新生成一個(gè)文件描述符newfd
,并使其指向舊文件描述符oldfd
所指向的files_struct
結(jié)構(gòu)體,即這兩個(gè)文件描述符共用一個(gè)files_struct
結(jié)構(gòu)體。
聲明:
NAME
close - close a file descriptor
SYNOPSIS
#include <unistd.h>
int close(int fd);
返回值:
- 成功:返回0;
- 出錯(cuò):則返回-1,并設(shè)置errno;
read()
功能: 從打開(kāi)的設(shè)備或文件中讀取數(shù)據(jù)。
聲明:
NAME
read - read from a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值:
- 成功:返回讀取的字節(jié)數(shù);
- 出錯(cuò):則返回-1,并設(shè)置errno;
- 如果在調(diào)read之前已到達(dá)文件末尾,則這次read返回0。
write()
功能:從內(nèi)存地址buf
開(kāi)始,向打開(kāi)的文件寫(xiě)入count
字節(jié)(byte)的數(shù)據(jù)。
聲明:
NAME
write - write to a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
返回值:
- 成功:返回寫(xiě)入的字節(jié)數(shù);
- 出錯(cuò):返回-1,并設(shè)置errno。
注意:
在向常規(guī)文件進(jìn)行寫(xiě)操作時(shí),write函數(shù)的返回值通常等于請(qǐng)求寫(xiě)的字節(jié)數(shù)count
,而向終端設(shè)備或網(wǎng)絡(luò)設(shè)備進(jìn)行寫(xiě)操作時(shí)則不一定。
Demo:mycp.c
程序功能描述:模仿cp命令,將一個(gè)文件中的內(nèi)容復(fù)制到一個(gè)新的文件之中。
code:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define SIZE 8192
int main(int argc, char *argv[])
{
int fd_src, fd_des, len;
char buf[SIZE];
/* 參數(shù)輸入太少,不符合要求,打印命令使用提示信息并退出 */
if (argc < 3) {
printf("Usage: ./mycp src_file des_file\n");
exit(1);
}
/* 打開(kāi)源文件 */
fd_src = open(argv[1], O_RDONLY);
if (fd_src == -1) {
printf("Openning file %s failed...\n", argv[1]);
exit(-1);
}
/* 新建目標(biāo)文件 */
fd_des = open(argv[2], O_CREAT | O_WRONLY | O_TRUNC, 0664);
if (fd_des == -1) {
printf("Creating file %s failed...\n", argv[2]);
exit(-1);
}
/* 讀取源文件中的內(nèi)容,然后寫(xiě)入目標(biāo)文件之中 */
while ( (len = read(fd_src, buf, sizeof(buf))) > 0 ) {
write(fd_des, buf, len);
}
/* 關(guān)閉文件 */
close(fd_src);
close(fd_des);
return 0;
}
test
slot@slot-ubt:~/test$ gcc mycp.c -o mycp
slot@slot-ubt:~/test$ cat aa
Hello, this is my cp cmd.
Welcome to use...
slot@slot-ubt:~/test$ cat bb
cat: bb: No such file or directory
slot@slot-ubt:~/test$ ./mycp aa bb
slot@slot-ubt:~/test$ cat bb
Hello, this is my cp cmd.
Welcome to use...
slot@slot-ubt:~/test$
lseek()
功能:移動(dòng)打開(kāi)的文件的讀寫(xiě)指針的位置。
每個(gè)打開(kāi)的文件都記錄著當(dāng)前讀寫(xiě)指針的位置,打開(kāi)文件時(shí)讀寫(xiě)位置是0,表示文件開(kāi)頭。通常,讀寫(xiě)多少個(gè)字節(jié),就會(huì)將讀寫(xiě)位置往后移動(dòng)多少個(gè)字節(jié)。但有一個(gè)例外,如果以
O_APPEND(追加)
方式打開(kāi),則每次寫(xiě)操作都會(huì)在文件末尾追加數(shù)據(jù),然后將讀寫(xiě)位置移到新的文件末尾。
聲明:
NAME
lseek - reposition read/write file offset
SYNOPSIS
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
lseek的兩個(gè)"副作用"示例
demo1. 擴(kuò)展一個(gè)文件
注意:
拓展一個(gè)文件,一定要有一個(gè)寫(xiě)操作。
code:extend_file.c
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
int fd;
/* 新建一個(gè)名為abc的文件 */
fd = open("abc", O_CREAT | O_RDWR);
if (fd < 0) {
perror("Opening file failed: ");
exit(-1);
}
/* 將讀寫(xiě)指針移到文件末尾 */
lseek(fd, 0x1000, SEEK_SET);
/* 追加寫(xiě)一個(gè)字節(jié)到文件中去
* string "a" will be translated to an addr
* od -tcx see file abc
*/
write(fd, "a", 1);
close(fd);
return 0;
}
errno
是個(gè)用戶態(tài)的全局變量,聲明在頭文件/usr/include/errno.h
中 :
45 #ifndef errno
46 extern int errno;
47 #endif
Linux下的錯(cuò)誤碼可以查閱文件:
/usr/include/asm-generic/errno-base.h
(部分展示如下):
1 #ifndef _ASM_GENERIC_ERRNO_BASE_H
2 #define _ASM_GENERIC_ERRNO_BASE_H
3
4 #define EPERM 1 /* Operation not permitted */
5 #define ENOENT 2 /* No such file or directory */
6 #define ESRCH 3 /* No such process */
7 #define EINTR 4 /* Interrupted system call */
8 #define EIO 5 /* I/O error */
9 #define ENXIO 6 /* No such device or address */
10 #define E2BIG 7 /* Argument list too long */
11 #define ENOEXEC 8 /* Exec format error */
12 #define EBADF 9 /* Bad file number */
13 #define ECHILD 10 /* No child processes */
14 #define EAGAIN 11 /* Try again */
15 #define ENOMEM 12 /* Out of memory */
16 #define EACCES 13 /* Permission denied */
perror()函數(shù)將打印用戶自定義信息和
errno
后面對(duì)應(yīng)的注釋信息,其聲明為:
NAME
perror - print a system error message
>
SYNOPSIS
#include <stdio.h>
>
void perror(const char *s);
>
#include <errno.h>
>
const char * const sys_errlist[];
int sys_nerr;
int errno;
test:
slot@slot-ubt:~/test$ gcc extend_file.c -o exf
slot@slot-ubt:~/test$ ./exf
slot@slot-ubt:~/test$ od -txc abc
0000000 00000000 00000000 00000000 00000000
\0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
*
0010000 00000061
a
0010001
demo2. 獲取文件的大小
方法:將指針移到文件末尾,然后輸出返回值,該值即文件大小。
code: see_file_size.c
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
int fd;
fd = open("abc", O_RDWR);
if (fd < 0) {
perror("Opening file failed: ");
exit(-1);
}
/* print file size */
printf("abc size is: %lld\n", lseek(fd, 0, SEEK_END));
close(fd);
return 0;
}
test:
slot@slot-ubt:~/test$ gcc see_file_size.c -o fsize
slot@slot-ubt:~/test$ ./fsize
abc size is: 4097
slot@slot-ubt:~/test$ ls -l abc
-rwxrwxrwx 1 slot staff 4097 12 14 19:40 abc
fcntl()
功能: 獲取或者設(shè)置已打開(kāi)文件的訪問(wèn)屬性。
聲明:
NAME
fcntl - manipulate file descriptor
SYNOPSIS
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
demo:
改變文件的狀態(tài)標(biāo)志位為非阻塞狀態(tài)
code: test_fcntl.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
int main()
{
char buf[10];
int n;
int flags;
/* get file flag */
flags = fcntl(STDIN_FILENO, F_GETFL);
/* change file flags to nonblock */
flags |= O_NONBLOCK;
if (fcntl(STDIN_FILENO, F_SETTL, flags) == -1) {
perror("change file flag failed: ");
exit(1);
}
try_again:
n = read(STDIN_FILENO, buf, 10);
if (n < 0) {
if (errno == EAGAIN) {
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto try_again;
}
perror("read stdin failed: ");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}
test:
ioctl()
功能:向設(shè)備發(fā)送控制和配置命令。
有些命令也需要讀寫(xiě)一些數(shù)據(jù),但這些數(shù)據(jù)是不能用read/write讀寫(xiě)的,稱為
Out-of-band
數(shù)據(jù)。也就是說(shuō),read/write讀寫(xiě)的數(shù)據(jù)是in-band
數(shù)據(jù),是I/O操作的主體,而ioctl命令傳送的是控制信息,其中的數(shù)據(jù)是輔助的數(shù)據(jù)。
例如,在串口線上收發(fā)數(shù)據(jù)通過(guò)read/write操作,而串口的波特率、校驗(yàn)位、停止位則通過(guò)ioctl來(lái)設(shè)置;A/D轉(zhuǎn)換(模數(shù)轉(zhuǎn)換)的結(jié)果通過(guò)read讀取,而A/D轉(zhuǎn)換的精度和工作頻率則通過(guò)ioctl設(shè)置。
聲明:
NAME
ioctl - control device
SYNOPSIS
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
fd
是某個(gè)設(shè)備的文件描述符,request
是ioctl的命令,可變參數(shù)取決于request
,通常是一個(gè)指向變量或結(jié)構(gòu)體的指針。
若出錯(cuò),則返回-1;若成功,則返回其他值。返回值也取決于
request
。
demo: 獲取終端窗口的大小
code: get_tty_size.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
int main(void)
{
struct winsize size;
/* 不是終端設(shè)備文件則退出 */
if (isatty(STDOUT_FILENO) == 0)
exit(1);
/* 通過(guò) ioctl() 獲取終端窗口的大小 */
if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) < 0) {
perror("ioctl TIOCGWINSZ error");
exit(1);
}
/* 打印終端窗口的長(zhǎng)和寬 */
printf("%d rows, %d columns\n", size.ws_row, size.ws_col);
return 0;
}
test:
(測(cè)試結(jié)果依賴于當(dāng)前終端的窗口大小)
slot@slot-ubt:~/test$ gcc get_tty_size.c -o ttysize
slot@slot-ubt:~/test$
slot@slot-ubt:~/test$ ./ttysize
24 rows, 65 columns
slot@slot-ubt:~/test$