WebServer1-Linux系统编程入门
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
2cd /etc/netplan
ls1
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 配置文件即可正常使用。免密登录:
- Windows 命令行下建立本地密钥
ssh-keygen -t rsa
- Linux 下建立密钥
ssh-keygen -t rsa
- 在
/home/user/.ssh/
下创建authorized_keys
,将 Windows 中的公钥复制到其中
重新在 Windows 的 VSCode 中连接虚拟机即可。
- Windows 命令行下建立本地密钥
2. gcc
2.1 Linux 安装 gcc 和 g++
sudo apt install gcc g++
2.2 gcc 工作流程
编译指令:
-
预处理文件:
1
2
3
4
5
6
7
8
9
10
11
12
13// test.c
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
下图可以看到引入了一些库和依赖
下图可以看到,预处理文件中将宏定义替换,将注释丢弃
-
编译文件:
gcc test.i -S -o test.s
生成汇编代码
-
汇编文件:
gcc test.s -s -o test.o
此时生成可执行文件
test.o
运行可执行文件
./test.o
-
链接文件:
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(组织)的一个编译器。
-
g++ 会调用 gcc ,对于 C++ 代码,gcc 命令不能自动和 C++ 程序使用的库联接,所以通常用 g++ 来完成链接,为了统一起见, cpp 代码的编译/链接都使用 g++。
-
gcc 不会定义
__cplusplus
宏,而 g++ 会。这个宏标志着编译器会将代码按照 C 还是 C++ 语法来解释。
-
编译可以用 gcc/g++,而链接可以用
g++
或者gcc -lstdc++
。
gcc 编译指令:
gcc -o a.out test.c
-g
用于 gdb 等调试使用
-D
用于调试输出
1 |
|
可以使用 Xftp 连接后进行左右拖动文件传输。
直接编译运行没有不输出,使用 gcc test.c -DDEBUG
或者 gcc test.c -D DEBUG
在文件中定义一个宏 DEBUG ,此时执行文件输出 Debuging...
。
-WALL
显示所有警告,例如在文件中添加一个变量定义 int a;
但是不使用,编译结果如下:
提示:
Ctrl + l
可以将命令行的指令推到第一行( •̀ ω •́ )✧
3. 静态库与动态库
-
库文件是一类二进制文件
-
库是特殊的一种程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行。
-
库文件有两种:静态库和动态库(共享库),区别是:
- 静态库在程序的链接阶段被复制到了程序中;
- 动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。
-
库的好处:
- 代码保密(C/C++ 反编译的还原度低,不像 Java 还原度高达 95% 以上)
- 方便部署和开发
3.1 静态库的制作
编写一个简单的加减乘除运算,制作一个静态库:
1 | fanqiyuan@fanqiyuan:~/Linux/lesson04$ tree |
1 | // add.c |
1 | // div.c |
1 | // head.h |
1 | // main.c |
1 | fanqiyuan@fanqiyuan:~/Linux/lesson04/calc$ gcc -c add.c sub.c mult.c div.c |
生成了静态库 libcalc.a
3.2 静态库的使用
如果让其他人使用自己写的库,需要将
include
中的头文件和lib
中的库文件一起发给对方,对方需要从.h
的声明中看出你的库实现了哪些功能,接口如何使用。
可以看一下 library 文件中的代码布局,include
中放头文件,lib
中放库文件,src
中放代码源文件。
编译程序:
下图可以看到 head.h
和 main.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
2fanqiyuan@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 | app:add.c sub.c mult.c div.c main.c |
4.3 Makefile 工作原理
-
命令执行之前,需要先检查规则中的依赖是否存在
- 如果存在,执行命令
- 如果不存在,向下检查其他的规则,检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该规则中的命令。
注意: 所有 Makefile 中所有程序都是为第一条规则所服务的,即后面出现与第一条规则相关的语句会被执行,否则不会被执行。
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17app: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 | #定义变量 |
4.5 模式匹配
使用模式匹配简化:
1 | #定义变量 |
删除所有 .o
文件,rm *.o
重新编译 make,正常执行
4.6 函数
使用函数进一步简化:
1 | src=$(wildcard ./*.c) |
4.7 clean
添加一个 clean 方法,用来清除中间文件:
1 | src=$(wildcard ./*.c) |
但是如果有一个同名文件的话:clean 就会与同名文件比较更新时间,但是 clean:
没有依赖,所以它的时间永远晚于自己的依赖:
为了避免这种影响,将 clean 操作设置为 伪目标 :
1 | src=$(wildcard ./*.c) |
即可正常执行!
5. GDB 调试
5.1 GDB 简介
一般来说,GDB主要帮助完成下面四个方面的功能:
- 启动程序,可以按照自定义的要求随心所欲的运行程序
- 可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式)
- 当程序被停住时,可以检查此时程序中所发生的事
- 可以改变程序,将一个 BUG 产生的影响修正从而测试其他 BUG
5.2 准备工作
通常在为调试而编译时,我们会关掉编译器的优化选项(-O
),并打开调试选项(-g
)。另外,-Wall
在尽量不影响程序行为的情况下选项打开所有 warning,也可以发现许多问题,避免一些不必要的 BUG。
gcc -g -Wall program.c -o program
-g
选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证 gdb 能找到源文件。
5.3 GDB 命令—启动、退出、查看代码
以调试 test.c
为例:
1 |
|
使用 -g
编译:gcc test.c -o test -g
将调试信息加入
普通编译:gcc test.c -o test1
查看两个可执行文件的信息发现,使用 -g
编译的可执行文件确实会大一些
-
启动gdb
gdb test
可以设置可执行文件的输入参数列表
-
查看程序
-
使用 vim 查看:
vim test.c
在 vim 界面中,输入:set nu
可以显示行号 -
在 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 | /* |
6.5 open 创建新文件
首先了解一下文件权限,ll
命令下可以看到每个文件的权限:最前面有10个字符,第一个代表文件类型,其余每三个一组,第一部分代表当前用户的权限,第二部分代表当前用户所在的组的权限,最后一部分代表其他组的权限。
权限设置以及其掩码的格式通常为八进制,对应权限情况如下例所示:
查看掩码以及设置掩码:(终端中设置的掩码会在重启后恢复默认值)
1 | /* |
执行代码可以看到创建了一个 create.txt
文件:
查看该文件的权限:(可以看到和我们设置的 umask 的情况保持一致)
6.6 read、write函数
函数使用例程如下:
1 | /* |
检查发现拷贝的文件和源文件大小相同,检查其内容也相同。
6.7 lseek函数
对比C标准库的fseek()
函数和Linux系统函数lseek()
:
区别就是C标准库用文件指针操作文件,而Linux系统函数用文件描述符操作。
1 | /* |
试想如果想在手机上下载一个 5G 的游戏,当下载了 99% 内存不够了,那该多痛苦!所以会预先扩展并占据 5G 的空间来保证数据的正常下载。
下面实现一个文件扩展的例子:
1 |
|
扩展前:
扩展后:
可以看到 hello.txt
中填充了很多空值。
6.8 stat, lstat函数
1 | /* |
stat 结构体:
可以直接使用命令行,利用stat
查看文件信息:
下面是利用stat
函数查看一个文件的大小实例:
1 |
|
再为 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 |
|
执行效果如下:
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
*/
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
*/
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);
可以利用如下方法查看用户名和组:
-
vim /etc/passwd
可以看到
x:用户id:组id
-
vim /etc/group
-
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
*/
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
*/
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: 数组大小
返回值:
返回指向的一块内存,这个数据就是第一个参数
*/
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 | /* |
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
从空闲文件描述符表中找一个最小的,作为新的拷贝的文件描述符
*/
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 必须是一个有效的文件描述符
*/
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.txt
的fd
没有关闭,仍然可以使用。
6.14 fcntl函数
-
int fcntl(int fd, int cmd, ... /* arg */ );
一共可以实现5中功能,这里主要掌握其中两种:
- 复制文件描述符
- 设置/获取文件的状态标志
1 | /* |
多次执行程序后,在原来数据后面追加 hello