1.linux man 1 2 3的作用

1
2
3
4
5
6
7
8
9
1、Standard commands (标准命令)
2、System calls (系统调用)
3、Library functions (库函数)
4、Special devices (设备说明)
5、File formats (文件格式)
6、Games and toys (游戏和娱乐)
7、Miscellaneous (杂项)
8、Administrative Commands (管理员命令)
9 其他(Linux特定的), 用来存放内核例行程序的文档。

说明:

系统调用 Linux内核提供的函数

库调用 c语言标准库函数和编译器特定库函数

例子:

man 1 cd

man 2 open

man 3 printf

一个小案例:

C 标准函数和系统函数调用关系。一个 helloworld 如何打印到屏幕。

2.open函数

2.1函数原型

manpage 第二卷(系统调用函数),输入==man 2 open==指令

open函数如下,有两个版本的

  • open是一个系统函数, 只能在linux系统中使用, windows不支持

  • fopen 是标准c库函数, 一般都可以跨平台使用, 可以这样理解:

    在linux中 fopen底层封装了Linux的系统API open

    在window中, fopen底层封装的是 window 的 api

2.2函数参数

  • pathname 文件路径

  • flags 文件打开方式:只读,只写,读写,创建,添加等。 O_RDONLY, O_WRONLY, O_RDWR,O_CREAT,O_APPEND,O_TRUNC,O_EXCL,O_NONBLOCK

  • mode参数,用来指定文件的权限,数字设定法。文件权限 = mode & ~umask。参数3使用的前提, 参2指定了 O_CREAT。 用来描述文件的访问权限。

2.3函数返回值

当open出错时,程序会自动设置errno,可以通过strerror(errno)来查看报错数字的含义

以打开不存在文件为例:

执行该代码,结果如下:

3.close()函数

3.1函数原型

int close(int fd)

3.2函数参数

  • fd 表示改文件的文件描述符,open的返回值
  • 返回值 成功为0 失败返回-1

小案例:

1
2
3
int  fd;
fd=open("tmp.txt",O_RDONLY);
close(fd);

4.read()函数

4.1函数原型

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

4.2函数参数

  • fd: 文件描述符,open () 函数的返回值,通过这个参数定位打开的磁盘文件
  • buf: 是一个传出参数,指向一块有效的内存,用于存储从文件中读出的数据
  • count: buf 指针指向的内存的大小,指定可以存储的最大字节数

4.3函数返回值

  • 大于 0: 从文件中读出的字节数,读文件成功
  • 等于 0: 代表文件读完了,读文件成功
  • -1: 读文件失败了,并设置 errno

如果返回-1: 并且 errno = EAGIN 或 EWOULDBLOCK, 说明不是read失败,而是read在以非阻塞方式读一个设备文件(网络文件),并且文件无数据。

5.write()函数

5.1函数原型

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

5.2函数参数

  • fd: 文件描述符,open () 函数的返回值,通过这个参数定位打开的磁盘文件
  • buf: 指向一块有效的内存地址,里边有要写入到磁盘文件中的数据
  • count: 要往磁盘文件中写入的字节数,一般情况下就是 buf 字符串的长度,strlen (buf)

5.3函数返回值

  • 大于 0: 成功写入到磁盘文件中的字节数
  • -1: 写文件失败了

6.小案例:用read()和write()函数实现copy功能

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
// 文件的拷贝
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main()
{
// 1. 打开存在的文件english.txt, 读这个文件
int fd1 = open("./english.txt", O_RDONLY);
if(fd1 == -1)
{
perror("open-readfile");
return -1;
}

// 2. 打开不存在的文件, 将其创建出来, 将从english.txt读出的内容写入这个文件中
int fd2 = open("copy.txt", O_WRONLY|O_CREAT, 0664);
if(fd2 == -1)
{
perror("open-writefile");
return -1;
}

// 3. 循环读文件, 循环写文件
char buf[4096];
int len = -1;
while( (len = read(fd1, buf, sizeof(buf))) > 0 )
{
// 将读到的数据写入到另一个文件中
write(fd2, buf, len);
}
// 4. 关闭文件
close(fd1);
close(fd2);

return 0;

}

7.系统调用和库函数比较—预读入缓输出

下面写两个文件拷贝函数,一个用read/write实现,一个用fputc/fgetc实现,比较他们两个之间的速度

==fputc/fgetc实现==

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main(void)
{
FILE *fp,*fp_out;
int n = 0;

fp = fopen("hello.c", "r");
if(fp == NULL)
{
perror("fopen error");
exit(1);
}
fp_out = fopen("hello.cp", "w");
if(fp_out == NULL)
{
perror("fopen error");
exit(1);
}
while((n = fgetc(fp)) != EOF)
{
fputc(n, fp_out);
}
fclose(fp);
fclose(fp_out);
return 0;
}

==下面修改read那边的缓冲区,一次拷贝一个字符。==

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
// 文件的拷贝
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
int n = 0;

int fd1 = open(argv[1], O_RDONLY);
if(fd1 == -1)
{
perror("open-readfile");
return -1;
}


int fd2 = open(argv[2], O_WRONLY|O_CREAT, 0664);
if(fd2 == -1)
{
perror("open-writefile");
return -1;
}

// 3. 循环读文件, 循环写文件
char buf[1];
int len = -1;
while( (len = read(fd1, buf, 1)) > 0 )
{
// 将读到的数据写入到另一个文件中
write(fd2, buf, len);
}
// 4. 关闭文件
close(fd1);
close(fd2);

return 0;

}

我猜很多人会觉得,read/write函数会比fputc/fgetc这些c语言标准库函数更快,因为read/write函数是系统调用函数,更接近linux内核。

其实不然,实际上fputc/fgetc会更快,为什么呢?下面我来分析一下

原因分析:

read/write,每次写一个字节,由于在用户区没缓冲区,会疯狂进行内核态和用户态的切换,所以非常耗时。

fgetc/fputc,在用户区有个缓冲区,所以它并不是一个字节一个字节地写进,内核和用户切换就比较少

所以系统函数并不是一定比库函数牛逼,能使用库函数的地方就使用库函数。

标准IO函数自带用户缓冲区,系统调用无用户级缓冲。系统缓冲区是都有的。

这就是预读入,缓输出机制

8.阻塞和非阻塞

阻塞、非阻塞: 是设备文件、网络文件的属性。

产生阻塞的场景。 读设备文件。读网络文件。(读常规文件无阻塞概念。)

/dev/tty – 终端文件。

open(“/dev/tty”, O_RDWR|O_NONBLOCK) — 设置 /dev/tty 非阻塞状态。(默认为阻塞状态)

小案例:从标准输入读,写到标准输出

执行程序,就会发现程序在阻塞等待输入

下面是一段更改非阻塞读取终端的代码:

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
#include <unistd.h>  
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "time out\n"

int main(void)
{
//打开文件
int fd, n, i;
fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);
if(fd < 0){
perror("open /dev/tty");
exit(1);
}
printf("open /dev/tty ok... %d\n", fd);


//轮询读取
char buf[10];
for (i = 0; i < 5; i++){
n = read(fd, buf, 10);
if (n > 0) { //说明读到了东西
break;
}
if (errno != EAGAIN) { //EWOULDBLOCK
perror("read /dev/tty");
exit(1);
} else {
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
sleep(2);
}
}

//超时判断
if (i == 5) {
write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
} else {
write(STDOUT_FILENO, buf, n);
}

//关闭文件
close(fd);
return 0;

}

执行,如图所示:

9.fcntl()函数

fcntl用来改变一个已经打开的文件的访问控制属性

重点掌握两个参数的使用, F_GETFLF_SETFL

9.1fcntl函数原型:

1
2
3
4
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

9.2函数参数:

  • fd 文件描述符
  • cmd 命令,决定了后续参数个数
  • 获取文件状态: F_GETFL
  • 设置文件状态: F_SETFL

9.3函数返回值

int flgs = fcntl(fd, F_GETFL);

flgs |= O_NONBLOCK

fcntl(fd, F_SETFL, flgs);

一个小案例:

终端文件默认是阻塞读的,这里用fcntl将其更改为非阻塞读:

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
#include <unistd.h>  
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MSG_TRY "try again\n"

int main(void)
{
char buf[10];
int flags, n;

flags = fcntl(STDIN_FILENO, F_GETFL); //获取stdin属性信息
if(flags == -1){
perror("fcntl error");
exit(1);
}
flags |= O_NONBLOCK;
int ret = fcntl(STDIN_FILENO, F_SETFL, flags);
if(ret == -1){
perror("fcntl error");
exit(1);
}

tryagain:
n = read(STDIN_FILENO, buf, 10);
if(n < 0){
if(errno != EAGAIN){
perror("read /dev/tty");
exit(1);
}
sleep(3);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
write(STDOUT_FILENO, buf, n);

return 0;

}

10.lseek()函数

系统函数 lseek 的功能是比较强大的,我们既可以通过这个函数移动文件指针,也可以通过这个函数进行文件的拓展。

10.1函数原型

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

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

10.2 函数参数

  • fd: 文件描述符,open () 函数的返回值,通过这个参数定位打开的磁盘文件
  • offset: 偏移量,需要和第三个参数配合使用
  • whence: 通过这个参数指定函数实现什么样的功能
    • SEEK_SET: 从文件头部开始偏移 offset 个字节
    • SEEK_CUR: 从当前文件指针的位置向后偏移 offset 个字节
    • SEEK_END: 从文件尾部向后偏移 offset 个字节

10.3 函数返回值

  • 成功:文件指针从头部开始计算总的偏移量
  • 失败: -1

一个小案例:

写一个句子到空白文件,完事调整光标位置,读取刚才写那个文件。

这个示例中,如果不调整光标位置,是读取不到内容的,因为读写指针在内容的末尾

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
#include <stdio.h>  
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main(void)
{
int fd, n;
char msg[] = "It's a test for lseek\n";
char ch;

fd = open("lseek.txt", O_RDWR|O_CREAT, 0644);
if(fd < 0){
perror("open lseek.txt error");
exit(1);
}

write(fd, msg, strlen(msg));

lseek(fd, 0, SEEK_SET);

while((n = read(fd, &ch, 1))){
if(n < 0){
perror("read error");
exit(1);
}
write(STDOUT_FILENO, &ch, n); //将文件内容按字节读出,写出到屏幕
}

close(fd);

return 0;

}

应用场景:

​ 1. 文件的“读”、“写”使用同一偏移位置。

​ 2. 使用lseek获取文件大小(返回值接收)

​ 3. 使用lseek拓展文件大小:要想使文件大小真正拓展,必须【引起IO操作】。

==小案例:==

用lseek的偏移来读取文件大小

用lseek实现文件拓展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// lseek.c
// 拓展文件大小
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
int fd = open("hello.txt", O_RDWR);
if(fd == -1)
{
perror("open");
return -1;
}

// 文件拓展, 一共增加了 1001 个字节
lseek(fd, 1000, SEEK_END);
write(fd, " ", 1);

close(fd);
return 0;

}

11.truncate()/ftruncate()函数

truncate/ftruncate 这两个函数的功能是一样的,可以对文件进行拓展也可以截断文件。使用这两个函数拓展文件比使用 lseek 要简单。这两个函数的函数原型如下:

1
2
3
4
5
6
7
// 拓展文件或截断文件
#include <unistd.h>
#include <sys/types.h>

int truncate(const char *path, off_t length);
-
int ftruncate(int fd, off_t length);

函数参数:

  • path: 要拓展 / 截断的文件的文件名
  • fd: 文件描述符,open () 得到的
  • length: 文件的最终大小
    • 文件原来 size > length,文件被截断,尾部多余的部分被删除,文件最终长度为 length
    • 文件原来 size < length,文件被拓展,文件最终长度为 length

函数返回值

成功返回 0; 失败返回值 - 1

truncate () 和 ftruncate () 两个函数的区别在于一个使用文件名一个使用文件描述符操作文件,功能相同。

不管是使用这两个函数还是使用 lseek () 函数拓展文件,文件尾部填充的字符都是 0。

小案例:

直接拓展文件。

1
int ret = truncate("dict.cp", 250);

12.目录项和inode

一个文件主要由两部分组成,dentry(目录项)和inode

inode本质是结构体,存储文件的属性信息,如:权限、类型、大小、时间、用户、盘快位置…

也叫做文件属性管理结构,大多数的inode都存储在磁盘上。

少量常用、近期使用的inode会被缓存到内存中。

所谓的删除文件,就是删除inode,但是数据其实还是在硬盘上,以后会覆盖掉。

13.stat()/lstate()函数

==想深入了解,请看这篇博客,下面只介绍常用的==

点我查看

用来获取文件或目录的详细属性信息包括文件系统状态,(从第二个参数结构体中获取)

13.1 函数原型

1
2
3
4
5
6
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);

==二者的区别:==

lstat (): 得到的是软连接文件本身的属性信息
stat (): 得到的是软链接文件关联的文件的属性信息(存在符号穿透)

13.2 函数参数

  • pathname: 文件名,要获取这个文件的属性信息
  • buf: 传出参数,文件的信息被写入到了这块内存中

这个函数的第二个参数是一个结构体类型,这个结构体相对复杂,通过这个结构体可以存储得到的文件的所有属性信息,结构体原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat {
dev_t st_dev; // 文件的设备编号
ino_t st_ino; // inode节点
mode_t st_mode; // 文件的类型和存取的权限, 16位整形数 -> 常用
nlink_t st_nlink; // 连到该文件的硬连接数目,刚建立的文件值为1
uid_t st_uid; // 用户ID
gid_t st_gid; // 组ID
dev_t st_rdev; // (设备类型)若此文件为设备文件,则为其设备编号
off_t st_size; // 文件字节数(文件大小) --> 常用
blksize_t st_blksize; // 块大小(文件系统的I/O 缓冲区大小)
blkcnt_t st_blocks; // block的块数
time_t st_atime; // 最后一次访问时间
time_t st_mtime; // 最后一次修改时间(文件内容)
time_t st_ctime; // 最后一次改变时间(指属性)
};

13.3 各种操作

获取文件大小
下面调用 stat () 函数,以代码的方式演示一下如何得到某个文件的大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <sys/stat.h>

int main()
{
// 1. 定义结构体, 存储文件信息
struct stat myst;
// 2. 获取文件属性 english.txt
int ret = stat("./english.txt", &myst);
if(ret == -1)
{
perror("stat");
return -1;
}

printf("文件大小: %d\n", (int)myst.st_size);

return 0;

}

获取文件类型
文件的类型信息存储在 struct stat 结构体的 st_mode 成员中,它是一个 mode_t 类型,本质上是一个 16 位的整数。Linux API 中为我们提供了相关的宏函数,通过对应的宏函数可以直接判断出文件是不是某种类型,这些信息都可以通过 man 文档(man 2 stat)查询到。

相关的宏函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 类型是存储在结构体的这个成员中: mode_t  st_mode;  
// 这些宏函数中的m 对应的就是结构体成员 st_mode
// 宏函数返回值: 是对应的类型返回-> 1, 不是对应类型返回0

S_ISREG(m) is it a regular file?
- 普通文件
S_ISDIR(m) directory?
- 目录
S_ISCHR(m) character device?
- 字符设备
S_ISBLK(m) block device?
- 块设备
S_ISFIFO(m) FIFO (named pipe)?
- 管道
S_ISLNK(m) symbolic link? (Not in POSIX.1-1996.)
- 软连接
S_ISSOCK(m) socket? (Not in POSIX.1-1996.)
- 本地套接字文件

在程序中通过宏函数判断文件类型,实例代码如下:

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
int main()
{
// 1. 定义结构体, 存储文件信息
struct stat myst;
// 2. 获取文件属性 english.txt
int ret = stat("./hello", &myst);
if(ret == -1)
{
perror("stat");
return -1;
}

printf("文件大小: %d\n", (int)myst.st_size);

// 判断文件类型
if(S_ISREG(myst.st_mode))
{
printf("这个文件是一个普通文件...\n");
}

if(S_ISDIR(myst.st_mode))
{
printf("这个文件是一个目录...\n");
}
if(S_ISLNK(myst.st_mode))
{
printf("这个文件是一个软连接文件...\n");
}

return 0;

}

13.目录操作函数

13.1opendir()函数

在目录操作之前必须要先通过 opendir () 函数打开这个目录,函数原型如下:

1
2
3
4
#include <sys/types.h>
#include <dirent.h>
// 打开目录
DIR *opendir(const char *name);
  • 参数: name -> 要打开的目录的名字
  • 返回值: DIR*, 结构体类型指针。打开成功返回目录的实例,打开失败返回 NULL

13.2readdir()函数

目录打开之后,就可以通过 readdir () 函数遍历目录中的文件信息了。每调用一次这个函数就可以得到目录中的一个文件信息,当目录中的文件信息被全部遍历完毕会得到一个空对象。先来看一下这个函数原型:

1
2
3
// 读目录
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
  • 参数:dirp -> opendir () 函数的返回值
  • 返回值:函数调用成功,返回读到的文件的信息,目录文件被读完了或者函数调用失败返回 NULL
  • 函数返回值 struct dirent 结构体原型如下:
1
2
3
4
5
6
7
struct dirent {
ino_t d_ino; /* 文件对应的inode编号, 定位文件存储在磁盘的那个数据块上 */
off_t d_off; /* 文件在当前目录中的偏移量 */
unsigned short d_reclen; /* 文件名字的实际长度 */
unsigned char d_type; /* 文件的类型, linux中有7中文件类型 */
char d_name[256]; /* 文件的名字 */
};

关于结构体中的文件类型 d_type,可使用的宏值如下

DT_BLK:块设备文件
DT_CHR:字符设备文件
DT_DIR:目录文件
DT_FIFO :管道文件
DT_LNK:软连接文件
DT_REG :普通文件
DT_SOCK:本地套接字文件
DT_UNKNOWN:无法识别的文件类型

通过 readdir () 函数遍历某一个目录中的文件:

1
2
3
4
5
6
7
8
// 打开目录
DIR* dir = opendir("/home/test");
struct dirent* ptr = NULL;
// 遍历目录
while( (ptr=readdir(dir)) != NULL)
{
.......
}

13.3closedir()函数

目录操作完毕之后,需要通过 closedir() 关闭通过 opendir() 得到的实例,释放资源。函数原型如下:

1
2
// 关闭目录, 参数是 opendir() 的返回值
int closedir(DIR *dirp);
  • 参数:dirp-> opendir () 函数的返回值
  • 返回值:目录关闭成功返回 0, 失败返回 -1

14.文件描述符复制和重定向(dup(),dup2()命令)

==请看这篇博客,写的很好==

点我查看

15.文件描述符

如上图所示,是我们的虚拟地址空间,其中0-3G为用户区,3-4G为内核区,其中我们的PCB进程控制块就在内核区,它的本质是一个结构体,在这个结构体中有一个成员变量 file_struct *file 指向文件描述符表。 从应用程序使用角度,该指针可理解记忆成一个字符指针数组,下标 0/1/2/3/4…找到文件结构体。本质是一个键值对 0、1、2…都分别对应具体地址。但键值对使用的特性是自动映射,我们只操作键不直接使用值。 新打开文件返回文件描述符表中未使用的最小文件描述符。
STDIN_FILENO 0 标准输入 (键盘)
STDOUT_FILENO 1 标准输出(显示器)
STDERR_FILENO 2 标准错误

一个进程默认打开文件的个数 1024。

16.写在最后

本文章是我在b站跟着黑马Linux系统编程学习时的笔记,为了防止遗忘和以后复习,写下这篇笔记,如有错误,欢迎指出

笔记中有的地方我是参考了这个博主的博客内容,这个博主的文章质量真的很高,这是它的博客点我查看,这是他的b站账号,有很多优质视频,强烈推荐点我查看

==黑马Linux系统编程视频链接==

点我查看