Web Server1

开始我的第一个 C++ 项目学习!

先跟着牛客视频学基础,写一个简单的 Webserver,然后再优化加入其他功能。

第一章 Linux 系统编程入门

1. 环境配置

VMware Workstation Pro 17 + Ubuntu 18.04

  • 问题1:虚拟机没网:

    将虚拟机网卡删除,重新添加

  • 问题2:虚拟机与主机之间文件拖动传输

    安装 VMware Tools -> 打开桌面上的CD文件 -> 将 .tar 文件放在桌面上解压 -> 运行其中的 vmware-install.pl 文件,安装相关依赖。这时应该可以正常使用了。

    如果还是不能拖动复制文件,打开任务管理器 -> 服务 -> 找到 vmware 开头的所有服务,将未启动的服务手动启动,重启虚拟机后,可以正常拖动复制。

    如果遇到权限问题无法操作,参考此文:【传送门

  • 问题3:Xshell 连接虚拟机,虚拟机中需要安装依赖

    sudo apt install openssh-server

    查看虚拟机 ip

    安装依赖: sudo apt install net-tools

    虚拟机命令行输入 ifconfig 查看

    Xshell 中创建一个会话并连接。

    后面使用过程中 ip 隔段时间就会变一下,还得重新配置,重新连接,很麻烦,直接设置成静态 ip。

    记住上面的子网IP、子网掩码、网关IP

    1
    2
    cd /etc/netplan
    ls
    1
    sudo vim 01-network-manager-all.yaml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # Let NetworkManager manage all devices on this system
    network:
    version: 2
    renderer: NetworkManager
    ethernets:
    ens33: #配置的网卡名称,使用ifconfig -a查看得到
    dhcp4: no #dhcp4关闭
    addresses: [192.168.88.139/24] #设置本机IP及掩码
    gateway4: 192.168.88.2 #设置网关
    nameservers:
    addresses: [192.168.88.2] #设置DNS

    使用如下命令生效

    1
    sudo netplan apply

    再次查看发现虚拟机的 ip 已经变成自己设置的静态 ip 了

  • VScode 连接虚拟机失败

    删除 C:\Users\awellfrog\.ssh\config ,重新修改 VScode 中的 Remote-SSH 配置文件即可正常使用。

    免密登录:

    1. Windows 命令行下建立本地密钥 ssh-keygen -t rsa
    2. Linux 下建立密钥 ssh-keygen -t rsa
    3. /home/user/.ssh/ 下创建 authorized_keys ,将 Windows 中的公钥复制到其中

    重新在 Windows 的 VSCode 中连接虚拟机即可。

2. gcc

2.1 Linux 安装 gcc 和 g++

sudo apt install gcc g++

2.2 gcc 工作流程

编译指令:

  1. 预处理文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // test.c
    #include <stdio.h>

    #define PI 3.14

    int main()
    {
    // 这是测试代码
    int sum = PI + 10;

    printf("Hello World!\n");
    return 0;
    }

    gcc test.c -E -o test.i // 预处理、链接,产生预处理文件 test.i

    可以在 VS code 中看到生成的文件 test.i

    下图可以看到引入了一些库和依赖

    下图可以看到,预处理文件中将宏定义替换,将注释丢弃

  2. 编译文件:

    gcc test.i -S -o test.s

    生成汇编代码

  3. 汇编文件:

    gcc test.s -s -o test.o

    此时生成可执行文件 test.o

    运行可执行文件 ./test.o

  4. 链接文件:

    gcc test.s -o test.out

    运行可执行文件 ./test.out

如果跨过某个步骤使用后面的指令直接编译,则前面的步骤会被包含完成。

gcc test.c -S 同时完成预处理,并生成汇编代码 test.s

gcc test.c 同时完成预处理、编译、汇编、链接,生成可执行文件 a.out

2.3 关于 gcc 和 g++

gcc 和 g++ 都是 GNU(组织)的一个编译器。

  1. g++ 会调用 gcc ,对于 C++ 代码,gcc 命令不能自动和 C++ 程序使用的库联接,所以通常用 g++ 来完成链接,为了统一起见, cpp 代码的编译/链接都使用 g++。

  2. gcc 不会定义 __cplusplus 宏,而 g++ 会。

    这个宏标志着编译器会将代码按照 C 还是 C++ 语法来解释。

  3. 编译可以用 gcc/g++,而链接可以用 g++ 或者 gcc -lstdc++

gcc 编译指令:

gcc -o a.out test.c

-g 用于 gdb 等调试使用

-D 用于调试输出

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main()
{
#ifdef DEBUG
printf("Debuging...\n");
#endif /* DEBUG */

return 0;
}

可以使用 Xftp 连接后进行左右拖动文件传输。

直接编译运行没有不输出,使用 gcc test.c -DDEBUG 或者 gcc test.c -D DEBUG 在文件中定义一个宏 DEBUG ,此时执行文件输出 Debuging...

-WALL 显示所有警告,例如在文件中添加一个变量定义 int a; 但是不使用,编译结果如下:

提示:

Ctrl + l 可以将命令行的指令推到第一行( •̀ ω •́ )✧

3. 静态库与动态库

  • 库文件是一类二进制文件

  • 库是特殊的一种程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行。

  • 库文件有两种:静态库和动态库(共享库),区别是:

    • 静态库在程序的链接阶段被复制到了程序中;
    • 动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。
  • 库的好处:

    1. 代码保密(C/C++ 反编译的还原度低,不像 Java 还原度高达 95% 以上)
    2. 方便部署和开发

3.1 静态库的制作

编写一个简单的加减乘除运算,制作一个静态库:

1
2
3
4
5
6
7
8
9
10
fanqiyuan@fanqiyuan:~/Linux/lesson04$ tree
.
├── calc
│   ├── add.c
│   ├── div.c
│   ├── head.h
│   ├── main.c
│   ├── mult.c
│   └── sub.c
└── library
1
2
3
4
5
6
7
8
// add.c
#include <stdio.h>
#include "head.h"

int add(int a, int b)
{
return a + b;
}
1
2
3
4
5
6
7
8
// div.c
#include <stdio.h>
#include "head.h"

double divide(int a, int b)
{
return (double)a / b;
}
1
2
3
4
5
// head.h
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
double divide(int a, int b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// main.c
#include <stdio.h>
#include "head.h"

int main()
{
int a = 20;
int b = 12;
printf("a = %d, b = %d\n", a, b);
printf("a + b = %d\n", add(a, b));
printf("a - b = %d\n", subtract(a, b));
printf("a * b = %d\n", multiply(a, b));
printf("a / b = %lf\n", divide(a, b));

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fanqiyuan@fanqiyuan:~/Linux/lesson04/calc$ gcc -c add.c sub.c mult.c div.c 
fanqiyuan@fanqiyuan:~/Linux/lesson04/calc$ ls
add.c add.o div.c div.o head.h main.c mult.c mult.o sub.c sub.o
fanqiyuan@fanqiyuan:~/Linux/lesson04/calc$ ar rcs libcalc.a add.o sub.o mult.o div.o
fanqiyuan@fanqiyuan:~/Linux/lesson04/calc$ tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── head.h
├── libcalc.a
├── main.c
├── mult.c
├── mult.o
├── sub.c
└── sub.o

生成了静态库 libcalc.a

3.2 静态库的使用

如果让其他人使用自己写的库,需要将 include 中的头文件和 lib 中的库文件一起发给对方,对方需要从 .h 的声明中看出你的库实现了哪些功能,接口如何使用。

可以看一下 library 文件中的代码布局,include 中放头文件,lib 中放库文件,src 中放代码源文件。

编译程序:

下图可以看到 head.hmain.c 不在一个目录下了,找不到

使用编译选项 -I 指定文件的搜索目录:但是找不到库文件对应的内容(libcalc.a 找不到)

使用编译选项 -l 链接数据库,后面跟库的名称即可:还是找不到名为 calc

使用编译选项 -L 指定要搜索的库的路径:(也可以先指定库的路径再链接数据库)

代码编译成功!

3.3 动态库的制作和使用

动态库生成完毕,下面使用动态库:

让他人使用自己写的动态库,则将动态库和头文件一同发给对方。

使用如下命令编译程序,并链接动态库

gcc main.c -o main -I./include -L./lib -l calc

下图可以看出动态库链接失败:

3.4 动态库加载失败的原因

了解一下库加载的原理:

  • 静态库:GCC 进行链接时,会把静态库中代码打包到可执行程序中

  • 动态库:GCC 进行链接时,动态库的代码不会被打包到可执行程序中

  • 程序启动之后,动态库会被动态加载到内存中,通过 ldd (list dynamic dependencies)命令检查动态库依赖关系

  • 如何定位共享库文件?

    当系统加载可执行代码时,能够知道其所依赖的库的名字,但是还需要知道绝对路径。此时就需要系统的动态载入器来获取该绝对路径。对于elf格式的可执行程序,是由 ld-linux.so 来完成的。它先后搜索 elf 文件的 DT_RPATH段 —> 环境变量LD_LIBRARY_PATH —> /etc/ld.so.cache 文件列表 —> /lib/, /usr/lib 目录找到库文件后将其载入内存。

    elf 文件的 DT_RPATH段,是一个进程启动时虚拟内存中对应内容,我们无法改动。

3.5 解决动态库加载失败问题

添加环境变量的方式:

  • 添加临时环境变量:

    查看动态库所在的文件目录:

    1
    2
    fanqiyuan@fanqiyuan:~/Linux/lesson06/library/lib$ pwd
    /home/fanqiyuan/Linux/lesson06/library/lib

    将其添加到环境变量中:

    1
    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/fanqiyuan/Linux/lesson06/library/lib

    查看环境变量,发现刚才的路径已经添加进去了:

    但是,这种方式只是临时加入环境变量,重启后需要重新添加。

  • 添加用户级环境变量:

    首先返回根目录:

    可以看到有个 .bashrc 文件,里面存的是用户的环境变量

    在该文件中添加环境变量:

    vim .bashrc

    保存并退出,然后执行更新后的环境变量:

    . .bashrc 或者 source .bashrc

    回到工程文件夹下,查看动态库链接成功:

  • 系统环境变量配置:

    同样,在系统环境变量中添加该路径即可。

    sudo vim /etc/profile

修改/etc/ld.so.cache 文件列表:

回到根目录下,查看该文件列表是否存在:下图可见存在

vim /etc/ld.so.cache 可以看出全是二进制代码无法修改,通过修改另一个文件实现对该文件列表的修改:

sudo vim /etc/ld.so.conf ,添加路径:

添加后更新:

sudo ldconfig

到工程文件夹下查看是否更新成功:

可以看到更新成功!

添加到 /lib/, /usr/lib 目录:

该方法不推荐,因为里面本身就包含了很多系统自带的库文件,如果出现重名,可能导致系统执行出现问题。

ll /lib huo ll /usr/lib 可以看到里面有很多系统自带的库文件。

3.6 静态库和动态库的对比

静态库的制作过程

动态库的制作过程

3.6.1 静态库的优缺点

优点:

  • 静态库被打包到应用程序中加载速度快
  • 发布程序无需提供静态库(已经加载在可执行文件中了),移植方便

缺点:

  • 消耗系统资源,浪费内存
  • 更新、部署、发布麻烦(修改程序后,开发者需要重新编译链接发给使用者,使用者也得对自己的工程更新和重新编译链接)

3.6.2 动态库的优缺点

优点:

  • 可以实现进程间资源共享(共享库):多个程序同时引用一个动态库时,只需要向内存中加载一次
  • 更新、部署、发布简单
  • 可以控制何时加载动态库

缺点:

  • 加载速度比静态库慢(稍慢)
  • 发布程序时需要提供依赖的动态库

4. Makefile

4.1 什么是 Makefile?

  • Makefile 文件定义了一系列的规则来指定那些文件需要先编译,那些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 Makefile 可以执行一些 Shell 脚本,也可以执行操作系统的命令。
  • Makefile 带来的好处就是“自动化编译”,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。make 是一个命令工具,是一个解释 Makefile 文件中指令的命令工具,一般来说,大多数的 IDE 都有这个命令,比如 Delphi 的 make, Visual C++ 的 nmake,Linux 下 GNU 的 make。

4.2 文件命名和规则

文件命名:

makefile 或 Makefile

Makefile 规则:

例:

写入 Makefile:

1
2
app:add.c sub.c mult.c div.c main.c
gcc add.c sub.c mult.c div.c main.c -o app

4.3 Makefile 工作原理

  • 命令执行之前,需要先检查规则中的依赖是否存在

    • 如果存在,执行命令
    • 如果不存在,向下检查其他的规则,检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该规则中的命令。

    注意: 所有 Makefile 中所有程序都是为第一条规则所服务的,即后面出现与第一条规则相关的语句会被执行,否则不会被执行。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    app:add.o sub.o mult.o div.o main.o
    gcc add.o sub.o mult.o div.o main.o -o app

    add.o:add.c
    gcc -c add.c -o add.o

    sub.o:sub.c
    gcc -c sub.c -o sub.o

    mult.o:mult.c
    gcc -c mult.c -o mult.o

    div.o:div.c
    gcc -c div.c -o div.o

    main.o:main.c
    gcc -c main.c -o main.o
  • 检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间

    • 如果依赖的时间比目标的时间晚,需要重新生成目标
    • 如果依赖时间比目标时间早,目标不需要更新,对应规则中的命令不需要被执行

    例:

    对没有任何修改,刚刚编译过的工程再次使用 make:

    修改上面的 main.c 文件,添加一个换行,再重新 make

    可以看到,由于 main.o 的依赖 main.c 的更新时间晚于 main.o ,所以该语句重新编译,依赖 main.o 的规则重新执行,但是其他规则不重复执行。

    该方式省略了一些重复的工作,效率更高。

4.4 变量

使用定义变量简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#定义变量
src=add.o sub.o mult.o div.o main.o
target=app

$(target):$(src)
$(CC) $(src) -o $(target)

add.o:add.c
gcc -c add.c -o add.o

sub.o:sub.c
gcc -c sub.c -o sub.o

mult.o:mult.c
gcc -c mult.c -o mult.o

div.o:div.c
gcc -c div.c -o div.o

main.o:main.c
gcc -c main.c -o main.o

4.5 模式匹配

使用模式匹配简化:

1
2
3
4
5
6
7
8
#定义变量
src=add.o sub.o mult.o div.o main.o
target=app
$(target):$(src)
$(CC) $(src) -o $(target)

%.o:%.c
$(CC) -c $< -o $@

删除所有 .o 文件,rm *.o 重新编译 make,正常执行

4.6 函数

使用函数进一步简化:

1
2
3
4
5
6
7
8
src=$(wildcard ./*.c)
objs=$(patsubst %.c, %.o, $(src))
target=app
$(target):$(objs)
$(CC) $(objs) -o $(target)

%.o:%.c
$(CC) -c $< -o $@

4.7 clean

添加一个 clean 方法,用来清除中间文件:

1
2
3
4
5
6
7
8
9
10
11
src=$(wildcard ./*.c)
objs=$(patsubst %.c, %.o, $(src))
target=app
$(target):$(objs)
$(CC) $(objs) -o $(target)

%.o:%.c
$(CC) -c $< -o $@

clean:
rm $(objs) -f

但是如果有一个同名文件的话:clean 就会与同名文件比较更新时间,但是 clean: 没有依赖,所以它的时间永远晚于自己的依赖:

为了避免这种影响,将 clean 操作设置为 伪目标

1
2
3
4
5
6
7
8
9
10
11
12
13
src=$(wildcard ./*.c)
objs=$(patsubst %.c, %.o, $(src))
target=app
$(target):$(objs)
$(CC) $(objs) -o $(target)

%.o:%.c
$(CC) -c $< -o $@

# 表示clean是伪目标,不需要和外面的同名文件比较更新时间
.PHONY:clean
clean:
rm $(objs) -f

即可正常执行!

5. GDB 调试

5.1 GDB 简介

一般来说,GDB主要帮助完成下面四个方面的功能:

  1. 启动程序,可以按照自定义的要求随心所欲的运行程序
  2. 可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式)
  3. 当程序被停住时,可以检查此时程序中所发生的事
  4. 可以改变程序,将一个 BUG 产生的影响修正从而测试其他 BUG

5.2 准备工作

通常在为调试而编译时,我们会关掉编译器的优化选项(-O),并打开调试选项(-g)。另外,-Wall 在尽量不影响程序行为的情况下选项打开所有 warning,也可以发现许多问题,避免一些不必要的 BUG。

gcc -g -Wall program.c -o program

-g 选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证 gdb 能找到源文件。

5.3 GDB 命令—启动、退出、查看代码

以调试 test.c 为例:

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

int test(int a);

int main(int argc, char *argv[])
{
int a, b;
printf("argc = %d\n", argc);

if (argc < 3) {
a = 10;
b = 30;
} else {
a = atoi(argv[1]);
b = atoi(argv[2]);
}
printf("a = %d, b =%d\n", a, b);
printf("a + b = %d\n", a + b);

for (int i = 0; i < a; ++i) {
printf("i = %d\n", i);
// 函数调用
int res = test(i);
printf("res value = %d\n", res);
}

printf("TiHE END!!!\n");
return 0;
}

int test(int a)
{
int num = 0;
for (int i = 0; i < a; i++) {
num += i;
}
return num;
}

使用 -g 编译:gcc test.c -o test -g 将调试信息加入

普通编译:gcc test.c -o test1

查看两个可执行文件的信息发现,使用 -g 编译的可执行文件确实会大一些

  • 启动gdb

    gdb test

    可以设置可执行文件的输入参数列表

  • 查看程序

    1. 使用 vim 查看:

      vim test.c 在 vim 界面中,输入 :set nu 可以显示行号

    2. 在 gdb 中查看代码

      gdb test -> list / l 一次显示 10 行代码,直接换行或者回车显示接下来的 10 行。

      gdb 调试查看源代码,使用普通编译不行,必须带上 -g 才能带上代码信息。

5.4 断点操作

5.5 调试命令

6. Linux 基础

6.1 标准C库IO函数和Linux系统IO函数对比

  • 跨平台的方式:

    • Java 使用 Java 虚拟机来实现。在不同平台上有不同实现方式的 java 虚拟机,代码运行在 java 虚拟机上
    • C 语言底层封装调用对应操作系统的 API 来实现跨平台。
  • C的标准IO库文件指针包含一个缓冲区,不需要每次读写磁盘。

    适用于磁盘读写时提高效率的场景。

  • Linux系统IO每次调用都读写磁盘。

    适用于网络通信要求实时通信,而不是把数据放在缓冲区不发送。

标准C库IO函数和Linux系统IO函数关系:

6.2. 虚拟地址空间

6.3. 文件描述符

内核的PCB(Processor Control Blocks)为每个进程包含一个文件描述符表,文件描述符即 *FILE ,每个文件描述符表是一个存储文件指针的数组,大小为 1024,即一个进程最多同时打开 1024 个文件。

6.4. open 打开文件

输入命令 man 2 open 查看 linux 函数文档。

man 2 Linux_func_name Linux 的文档在第二页

man 3 C_func_name C 语言的标准库文档在第三页

比如要在文档中查询返回值,输入 /RETURN VALUE

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 <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

// 打开一个已创建的文件
int open(const char *pathname, int flags);
参数:
pathname: 要打开的文件路径
flags: 对文件的操作权限设置还有其他的设置
O_RDONLY, O_WRONLY, O_RDWR
These request opening the file read-only, write-only, or read/write, respectively.
返回值:return the new file descriptor,
or -1 if an error occurred (in which case, errno is set appropriately).

// 创建一个新的文件
int open(const char *pathname, int flags, mode_t mode);

*/

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>


int main() {
// 打开一个文件
int fd = open("a.txt", O_RDONLY);

if (fd == -1) {
perror("open");
}

// 关闭文件描述符
close(fd);

return 0;
}

6.5 open 创建新文件

首先了解一下文件权限,ll 命令下可以看到每个文件的权限:最前面有10个字符,第一个代表文件类型,其余每三个一组,第一部分代表当前用户的权限,第二部分代表当前用户所在的组的权限,最后一部分代表其他组的权限。

权限设置以及其掩码的格式通常为八进制,对应权限情况如下例所示:

查看掩码以及设置掩码:(终端中设置的掩码会在重启后恢复默认值)

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 <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);
参数:
- pathname: 要创建的文件的路径
- flags: 对文件的操作权限和其他的设置
- 必选项: O_RDONLY, O_WRONLY, O_RDWR
- 可选项: O_CREAT 文件不存在,则创建新文件
- mode: 八进制的数,表示创建处的新文件的操作权限,比如:0775
最终权限是: mode & ~umask (用户模式: umask=0002; 内核模式: umask=0022, 掩码是可以设置的)
0777 -> 1111111111
& 0775 -> 1111111101
-----------------------------
1111111101
umask 的作用就是抹去某些权限
*/

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>


int main() {
// 不存在的话创建一个新文件
int fd = open("create.txt", O_RDONLY | O_CREAT, 0777);

if (fd == -1) {
perror("open");
}

// 关闭文件描述符
close(fd);

return 0;
}

执行代码可以看到创建了一个 create.txt 文件:

查看该文件的权限:(可以看到和我们设置的 umask 的情况保持一致)

6.6 read、write函数

函数使用例程如下:

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
/*
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数:
- fd: 文件描述符,open得到的,通过这个文件描述符操作某个文件
- buf: 需要读取数据存放的地方,数组的地址(传出参数)
- count: 指定的数组的大小
返回值:
- 成功:
>0: 返回实际的读取到的字节数
=0: 文件已经读取完了
- 失败: -1,并且设置 errno

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数:
- fd: 文件描述符,open得到的,通过这个文件描述符操作某个文件
- buf: 要往磁盘写入的数据
- count: 要些的数据的实际大小
返回值:
- 成功: 实际写入的字节数
- 失败: -1,并且设置 errno

*/

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {

// 1. 通过open打开englist.txt文件
int srcfd = open("hello.c", O_RDONLY);
if (srcfd == -1) {
perror("open");
return -1;
}

// 2. 创建一个新的文件(拷贝文件)
int destfd = open("cpy.txt", O_WRONLY | O_CREAT, 0664);
if (destfd == -1) {
perror("open");
return -1;
}

// 3. 频繁的读写操作
char buf[1024] = {0};
int len;
while ((len = read(srcfd, buf, sizeof(buf))) > 0) {
write(destfd, buf, len);
}


// 4. 关闭文件
close(srcfd);
close(destfd);

return 0;
}

检查发现拷贝的文件和源文件大小相同,检查其内容也相同。

6.7 lseek函数

对比C标准库的fseek()函数和Linux系统函数lseek()

区别就是C标准库用文件指针操作文件,而Linux系统函数用文件描述符操作。

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
/*
标准C库函数
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);

Linux系统函数
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
参数:
-fd:文件描述符。通过open得到,通过这个fd操作某个文件
SEEK_SET
设置文件指针的偏移量
SEEK_CUR
设置偏移量:当前位置 + 第二个参数offset的值
SEEK_END
设置偏移量:文件大小 + 第二个参数offset的值
返回值:返回文件指针的位置

作用:
1. 移动文件指针到文件头
lseek(fd, 0, SEEK_SET);

2. 获取当前文件指针的位置
lseek(fd, 0, SEEK_CUR);

3. 获取文件长度
lseek(fd, 0, SEEK_END);

4. 拓展文件的长度,当前文件10b, 110b,增加了100个字节
lseek(fd, 100, SEEK_END);
注意:需要在文件尾写一次数据(写一个空字符)才能拓展成功

*/

试想如果想在手机上下载一个 5G 的游戏,当下载了 99% 内存不够了,那该多痛苦!所以会预先扩展并占据 5G 的空间来保证数据的正常下载。

下面实现一个文件扩展的例子:

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
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
// 打开文件
int fd = open("hello.txt", O_RDWR);

// 扩展文件
int ret = lseek(fd, 100, SEEK_END);
if (ret == -1) {
perror("lseek");
return -1;
}

// 写入一个空数据,如果没有此步则扩展失败
write(fd, " ", 1);

// 关闭文件
close(fd);

return 0;
}

扩展前:

扩展后:

可以看到 hello.txt 中填充了很多空值。

6.8 stat, lstat函数

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

int stat(const char *pathname, struct stat *statbuf);
作用:获取一个文件相关的一些信息
参数:
- pathname:
- statbuf: 结构题变量,传出参数,用于保存获取到的文件的信息


int lstat(const char *pathname, struct stat *statbuf);
作用:获取软链接的信息

*/

stat 结构体:

可以直接使用命令行,利用stat查看文件信息:

下面是利用stat函数查看一个文件的大小实例:

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

int main() {

struct stat statbuf;

int ret = stat("a.txt", &statbuf);

if (ret == -1) {
perror("stat");
return -1;
}

printf("size: %ld\n", statbuf.st_size);


return 0;
}

再为 a.txt 创建一个软链接 b.txt

1
ln -s a.txt b.txt

可以看到 b.txt 是一个指向 a.txt 的软链接。

命令行使用stat b.txt 看到的是软链接的信息,但是C代码调用stat()函数只能看到其所指向的文件a.txt的信息,并不能看到软链接的信息。想要在C代码中看到软链接的信息,则需要使用lstat()函数。

查看 b.txt 看到的是 a.txt 的内容。

6.9 模拟实现 ls -l 命令

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <string.h>

// 模拟实现 ls -l 指令
// -rw-r--r-- 1 fanqiyuan fanqiyuan 12 4月 21 16:51 a.txt


int main(int argc, char *argv[]) {

// 判断输入的参数是否正确
if (argc < 2) {
printf("%s filename\n", argv[0]);
return -1;
}

struct stat st;
int ret = stat(argv[1], &st);
if (ret == -1) {
perror("stat");
return -1;
}

// 获取文件类型和文件权限
char perms[11] = {0}; // 用于保存文件类型和文件权限的字符串

switch(st.st_mode & S_IFMT) {
case S_IFSOCK:
perms[0] = 's';
break;
case __S_IFLNK:
perms[0] = 'l';
break;
case __S_IFREG:
perms[0] = '-';
break;
case __S_IFBLK:
perms[0] = 'b';
break;
case __S_IFDIR:
perms[0] = 'd';
break;
case __S_IFCHR:
perms[0] = 'c';
break;
case __S_IFIFO:
perms[0] = 'p';
break;
case __S_IFMT:
perms[0] = 'm';
break;
default:
perms[0] = '?';
}

// 判断文件的访问权限

// 文件所有者
perms[1] = (st.st_mode & S_IRUSR) ? 'r' : '-';
perms[2] = (st.st_mode & S_IWUSR) ? 'w' : '-';
perms[3] = (st.st_mode & S_IXUSR) ? 'x' : '-';

// 文件所在组
perms[4] = (st.st_mode & S_IRGRP) ? 'r' : '-';
perms[5] = (st.st_mode & S_IWGRP) ? 'w' : '-';
perms[6] = (st.st_mode & S_IXGRP) ? 'x' : '-';

// 其他人
perms[7] = (st.st_mode & S_IROTH) ? 'r' : '-';
perms[8] = (st.st_mode & S_IWOTH) ? 'w' : '-';
perms[9] = (st.st_mode & S_IXOTH) ? 'x' : '-';

// 硬连接数
int linkNum = st.st_nlink;

// 文件所有者
char *fileUser = getpwuid(st.st_uid)->pw_name;

// 文件所在组
char *fileGrp = getgrgid(st.st_gid)->gr_name;

// 文件大小
long int fileSize = st.st_size;

// 修改时间
char *time = ctime(&st.st_mtime);
char mtime[1024] = {0};
strncpy(mtime, time, strlen(time) - 1);

char buf[1024];
sprintf(buf, "%s %d %s %s %ld %s %s", perms, linkNum, fileUser, fileGrp, fileSize, mtime, argv[1]);

printf("%s\n", buf);

return 0;
}

执行效果如下:

6.10 文件属性操作函数

  • int access(const char *pathname, int mode);

    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
    /*
    #include <unistd.h>
    int access(const char *pathname, int mode);
    作用:判断某个文件是否有某个权限,或者判断文件是否存在
    参数:
    - pathname: 判断的文件路径
    - mode:
    R_OK: 判断是否有读权限
    W_OK: 判断是否有写权限
    X_OK: 判断是否有执行权限
    F_OK: 判断文件是否存在
    返回值:成功返回 0,失败返回 -1
    */

    #include <unistd.h>
    #include <stdio.h>

    int main() {

    int ret = access("a.txt", F_OK);
    if (ret == -1) {
    perror("access");
    }

    printf("文件存在!!!\n");

    return 0;
    }
  • int chmod(const char *pathname, mode_t mode);

    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 <sys/stat.h>
    int chmod(const char *pathname, mode_t mode);
    作用:修改文件权限
    参数:
    - pathname: 需要修改的文件的路径
    - mode: 需要修改的权限值,八进制的数
    返回值:成功返回0,失败返回-1
    */

    #include <sys/stat.h>
    #include <stdio.h>

    int main() {

    int ret = chmod("a.txt", 0777);

    if (ret == -1) {
    perror("chmod");
    return -1;
    }

    return 0;
    }

    观察下图可以看到文件的权限发生变化从 0664 -> 0777

  • int chown(const char *pathname, uid_t owner, gid_t group);

    可以利用如下方法查看用户名和组:

    1. vim /etc/passwd

      可以看到 x:用户id:组id

    2. vim /etc/group

    3. id fanqiyuan

      直接查看当前用户的各种信息

  • int truncate(const char *path, off_t length);

    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 <unistd.h>
    #include <sys/types.h>
    int truncate(const char *path, off_t length);
    作用:缩减或者扩展文件的尺寸至指定的大小
    参数:
    - path: 需要修改的文件的路径
    - length: 需要最终文件变成的大小
    返回值:成功返回0,失败返回-1

    */

    #include <unistd.h>
    #include <sys/types.h>
    #include <stdio.h>

    int main() {

    int ret = truncate("b.txt", 20);

    if (ret == -1) {
    perror("truncate");
    return -1;
    }

    return 0;
    }

    将文件扩展为 20 字节:

    将文件缩减为5字节:

6.11 目录操作函数

  • int mkdir(const char *pathname, mode_t mode);

    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
    /*
    #include <sys/stat.h>
    #include <sys/types.h>
    int mkdir(const char *pathname, mode_t mode);
    作用:创建一个目录
    参数:
    - pathname: 创建目录的路径
    - mode: 权限,八进制数
    返回值:
    成功返回0,失败返回-1
    */

    #include <sys/stat.h>
    #include <sys/types.h>
    #include <stdio.h>

    int main() {
    int ret = mkdir("aaa", 0777);

    if (ret == -1) {
    perror("mkdir");
    return -1;
    }

    return 0;
    }

    执行程序后可以看到生成了一个目录:

  • int rmdir(const char *pathname);

    删除空目录

  • int rename(const char *oldpath, const char *newpath);

  • int chdir(const char *path);

    修改当前工作目录

  • char *getcwd(char *buf, size_t size);

    获取当前工作的路径

    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
    /*
    #include <unistd.h>
    int chdir(const char *path);
    作用:修改进程的工作目录
    比如在/home/fanqiyuan 启动了一个可执行程序,进程的工作目录就是/home/fanqiyuan
    参数:需要修改的工作目录

    #include <unistd.h>
    char *getcwd(char *buf, size_t size);
    作用:获取当前工作目录
    参数:
    - buf: 存储路径,指向的是一个数组(传出参数)
    - size: 数组大小
    返回值:
    返回指向的一块内存,这个数据就是第一个参数
    */

    #include <unistd.h>
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>

    int main() {
    // 获取当前工作目录
    char buf[128];
    getcwd(buf, sizeof(buf));
    printf("当前的工作目录是:%s\n", buf);

    // 修改工作目录
    int ret = chdir("/home/fanqiyuan/Linux/lesson13");
    if (ret == -1) {
    perror("chdir");
    return -1;
    }

    // 创建一个新的文件
    int fd = open("chdir.txt", O_CREAT | O_RDWR, 0664);
    if (fd == -1) {
    perror("open");
    return -1;
    }

    close(fd);

    // 获取当前工作目录
    char buf1[128];
    getcwd(buf1, sizeof(buf1));
    printf("当前的工作目录是:%s\n", buf1);

    return 0;
    }

6.12 目录遍历函数

  • DIR *opendir(const char *name);
  • struct dirent *readdir(DIR *dirp);
  • int closedir(DIR *dirp);
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
/*
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
参数:
- name: 需要打开的目录的名称
返回值:
DIR * 类型,理解为目录流
错误返回 NULL

// 读取目录中的数据
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
- 参数: dirp是opendir返回的结果
- 返回值:
struct dirent,代表读取到的文件信息
struct dirent {
ino_t d_ino; // Inode number
off_t d_off; // Not an offset; see below
unsigned short d_reclen; // Length of this record
unsigned char d_type; // Type of file; not supported by all filesystem types
char d_name[256]; // Null-terminated filename
};
如果读取到了文件的末尾,返回NULL

// 关闭目录
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);

*/

#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int getFileNum(const char *path);

int main(int argc, char *argv[]) {

if (argc < 2) {
printf("%s path\n", argv[0]);
return -1;
}

int num = getFileNum(argv[1]);
printf("目录%s下的普通文件个数是:%d\n", argv[1], num);

return 0;
}

int debug_flag = 0;
// 用于获取目录下所有普通文件的个数
int getFileNum(const char *path) {
// 1. 打开目录
DIR *dir = opendir(path);

if (dir == NULL) {
perror("opendir");
exit(0);
}

struct dirent *ptr;

// 记录文件夹下普通文件的数量
int fileNum = 0;

// 目录流是否读完
while ((ptr = readdir(dir)) != NULL) {

// 获取名称
char *dname = ptr->d_name;
if (strcmp(dname, ".") == 0 || strcmp(dname, "..") == 0) continue;

// 如果是文件夹
if (ptr->d_type == DT_DIR) {
// 又找到一个文件夹,递归统计
char newPath[256];
sprintf(newPath, "%s/%s", path, dname);
fileNum += getFileNum(newPath);
}

// 如果是普通文件
if (ptr->d_type == DT_REG) {
fileNum++;
}
}

// 关闭目录
closedir(dir);

return fileNum;
}

6.13 dup、dup2函数

  • int dup(int oldfd);

    作用:复制文件描述符

    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
    /*
    #include <unistd.h>
    int dup(int oldfd);
    作用:复制文件描述符
    fd = 3, int fd1 = dip(fd);
    fd指向a.txt,fd1也是指向a.txt
    从空闲文件描述符表中找一个最小的,作为新的拷贝的文件描述符

    */

    #include <unistd.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <string.h>

    int main() {

    int fd = open("a.txt", O_RDWR | O_CREAT, 0664);

    int fd1 = dup(fd);

    if (fd1 == -1) {
    perror("dup");
    return -1;
    }

    printf("fd : %d, fd1 : %d\n", fd, fd1);

    close(fd);

    char *str = "hello,world";
    int ret = write(fd1, str, strlen(str));

    if (ret == -1) {
    perror("write");
    return -1;
    }

    close(fd1);
    return 0;
    }

    从上述代码的执行中可以看出,两个文件描述符独立存在,指向同一个文件。

  • int dup2(int oldfd, int newfd);

    作用:重定向文件描述符

    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
    /*
    #include <unistd.h>
    int dup2(int oldfd, int newfd);
    作用:重定向文件描述符
    oldfd 指向 a.txt, newfd 指向 b.txt
    调用成功后,newfd 和 b.txt 均 close, newfd 指向了 a.txt
    oldfd 必须是一个有效的文件描述符
    */

    #include <unistd.h>
    #include <stdio.h>
    #include <string.h>
    #include <fcntl.h>
    #include <sys/types.h>
    #include <sys/stat.h>

    int main() {

    int fd = open("1.txt", O_RDWR | O_CREAT, 0664);
    if (fd == -1) {
    perror("open");
    return -1;
    }

    int fd1 = open("2.txt", O_RDWR | O_CREAT, 0664);
    if (fd1 == -1) {
    perror("open");
    return -1;
    }

    printf("dup : %d ; dup2 : %d\n", fd, fd1);

    int fd2 = dup2(fd, fd1);
    if (fd2 == -1) {
    perror("dup2");
    return -1;
    }

    // 通过fd1去写数据,实际操作的是1.txt,而不是2.txt
    char *str = "Hello, dup2\n";
    int len = write(fd1, str, strlen(str));

    if (len == -1) {
    perror("write");
    return -1;
    }

    // 查看 fd 是否被关闭
    char *str0 = "Hello, dup2, I'm fd\n";
    int len0 = write(fd, str0, strlen(str0));

    if (len0 == -1) {
    perror("write fd");
    return -1;
    }

    printf("fd : %d ; fd1 : %d; fd2 : %d\n", fd, fd1, fd2);

    close(fd);
    close(fd1);

    return 0;
    }

    从上面代码的执行情况可以看出,fd1本来指向 2.txt ,但是经过 dup2() 后,指向了 1.txt 。并且原来指向 a.txtfd 没有关闭,仍然可以使用。

6.14 fcntl函数

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

    一共可以实现5中功能,这里主要掌握其中两种:

    1. 复制文件描述符
    2. 设置/获取文件的状态标志
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
/*
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ...);
参数:
fd : 表示需要操作的文件描述符
cmd: 表示对文件描述符进行如何操作
- F_DUPFD : 复制文件描述符,复制的第一个参数fd,得到一个新的文件描述符
int ret = fcntl(fd, F_DUPFD);

- F_GETFL : 获取指定的文件描述符文件状态flag
获取的flag和通过open函数传递的flag是一个东西

- F_SETFL : 设置文件描述符状态flag
必选项:O_RDONLY, O_WRONLY, O_RDWR
可选项:O_APPEND, O_NONBLOCK
O_APPEND 表示追加数据(追加之前要把原来的数据读出来,否则会被直接覆盖)
O_NONBLOCK 设置成非阻塞
阻塞和非阻塞:描述的是函数的行为。(终端等待用户输入就是一种阻塞行为)
*/

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main() {

// 1. 复制文件描述符
// int fd = open("1.txt", O_RDONLY);
// int ret = fcntl(fd, F_DUPFD);

// 2. 修改或者获取文件状态flag
int fd = open("1.txt", O_RDWR); // 追加需要写权限,不能是 O_RDONLY
if (fd == -1) {
perror("open");
return -1;
}

// 获取文件描述符
int flag = fcntl(fd, F_GETFL);

// 修改文件描述符标flag,给flag加入O_APPEND标记
flag |= O_APPEND;
int ret = fcntl(fd, F_SETFL, flag);

char *str = "hello ";
ret = write(fd, str, strlen(str));
if (ret == -1) {
perror("write");
return -1;
}

close(fd);

return 0;
}

多次执行程序后,在原来数据后面追加 hello