linux系统编程入门

一、搭建环境

1、安装 Linux 系统(虚拟机安装、云服务器)

https://releases.ubuntu.com/bionic/

2、安装 XSHELL、XFTP

https://www.netsarang.com/zh/free-for-home-school/

3、安装 visual studio code

https://code.visualstudio.com/

4、Linux环境配置

安装 VMTools

在这里插入图片描述

解压 vmware-tools-distrib 到桌面,进入文件夹后,用命令行打开,并输入下面命令进行安装

sudo ./vmware-install.pl

在这里插入图片描述

安装 openssh-server

这里提示出了,依赖的openssh-client包版本高了,安装不了openssh-server

openssh-server : 依赖: openssh-client (= 1:7.6p1-4)

因此需要重新安装这个包,也就是给系统里这个包降级,将如上的版本号跟在安装包后面指定要安装的版本

正在读取软件包列表... 完成
正在分析软件包的依赖关系树       
正在读取状态信息... 完成       
有一些软件包无法被安装。如果您用的是 unstable 发行版,这也许是
因为系统无法达到您要求的状态造成的。该版本中可能会有一些您需要的软件
包尚未被创建或是它们已被从新到(Incoming)目录移出。
下列信息可能会对解决问题有所帮助:

下列软件包有未满足的依赖关系:
 openssh-server : 依赖: openssh-client (= 1:7.6p1-4)
                  依赖: openssh-sftp-server 但是它将不会被安装
                  推荐: ssh-import-id 但是它将不会被安装
E: 无法修正错误,因为您要求某些软件包保持现状,就是它们破坏了软件包间的依赖关系
sudo apt-get install openssh-client=1:7.6p1-4
sudo apt install -y openssh-server

安装 net-tools

安装后就可以使用 ifconfig 命令来查看本机IP

sudo apt install net-tools

安装 vim

sudo apt-get purge vim-common
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install vim

5、Visual Studio Code 安装相关插件

Chinese (Simplified) (简体中文) Language
Remote Development
C/C++ Extension Pack
Rainbow Brackets

安装完 Remote Development 插件后需要进行配置

在这里插入图片描述

在这里插入图片描述

修改为自己 Linux 的相关配置信息

先查看 Ubuntu 的 IP 地址信息

在这里插入图片描述

# Read more about SSH config files: https://linux.die.net/man/5/ssh_config
Host Ubuntu18_1
    HostName 192.168.182.128
    User qykhhr

点击 文件夹样式按钮就能通过 SSH 连接 Linux 服务器

在这里插入图片描述

点击后就会打开一个新的 VS code ,提示用户输入 Linux 的密码,输入密码后,就能正常连接 Linux 服务器了

在这里插入图片描述

我们在 home 目录下创建 Linux 目录,然后用 VS code 打开这个目录

mkdir /home/qykhhr/Linux

在这里插入图片描述

配置 SSH 免密登录

Windows

使用命令在本机 C盘用户目录下生成 ssh 密钥

ssh-keygen -t rsa

在这里插入图片描述

Linux

使用命令在 Linux 用户目录下生成 ssh 密钥

ssh-keygen -t rsa

在这里插入图片描述

在生成的 .ssh 目录下创建 authorized_keys 文件,将 Windows 下产生的公钥写入到这个文件中

vim authorized_keys

我们再使用VSCode连接 Linux 服务器就不会在提示输入密码了。

二、GCC

1、简介

  • GCC 原名为 GNU C语言编译器(GNU C Compiler)
  • GCC(GNU Compiler Collection,GNU编译器套件)是由 GNU 开发的编程语言译器。GNU 编译器套件包括 C、C++、ObjectIve-C、Java、Ada 和 Go 语言前端,也包括了这些语言的库(如 libstdc++、libgcj 等)
  • GCC 不仅支持 C 的许多“方言”,也可以区别不同的 C 语言标准;可以使用命令行选项来控制编译器在翻译源代码时应该遵守哪个 C 标准。例如:当使用命令行参数 -std=c99 启动 GCC 时,编译器支持 C99 标准。
  • 安装命令 sudo apt install -y gcc g++(版本大于 4.8.5)
  • 查看版本 gcc/g++ -v/--version

2、安装 GCC、G++

安装g++时遇到了如下问题。 输入sudo apt-get install g++后:

qykhhr@qykhhr:~/.ssh$ sudo apt install -y gcc
正在读取软件包列表... 完成
正在分析软件包的依赖关系树       
正在读取状态信息... 完成       
有一些软件包无法被安装。如果您用的是 unstable 发行版,这也许是
因为系统无法达到您要求的状态造成的。该版本中可能会有一些您需要的软件
包尚未被创建或是它们已被从新到(Incoming)目录移出。
下列信息可能会对解决问题有所帮助:

下列软件包有未满足的依赖关系:
 gcc : 依赖: gcc-7 (>= 7.3.0-12~) 但是它将不会被安装
       推荐: libc6-dev 但是它将不会被安装 或
               libc-dev
E: 无法修正错误,因为您要求某些软件包保持现状,就是它们破坏了软件包间的依赖关系。

为了解决这个问题,上网一搜,发现是 Ubuntu 安装的软件包版本高,而所安装软件的依赖包版本低的原因。解决方法:

终端输入:

sudo apt-get install aptitude
sudo aptitude install gcc g++

注意在执行 sudo aptitude install g++ 命令时,我们需要第一次选 no ,第二次再选 yes

若此处选择n的话会给你提供其他方案。当然根据自身情况不同,可能需要选择的方案也不同。

在这里插入图片描述

在这里插入图片描述

此处可以看见gcc已成功降级,g++也安装成功:

在这里插入图片描述

3、编译 hello world

先创建要给 test.c 文件,写入以下内容:

#include<stdio.h>
int main()
{
    printf("hello world\n");
}

使用下面命令来编译生成一个运行文件

gcc test.c -o app

在这里插入图片描述

如果直接使用 gcc test.c 会默认生成 a.out 可执行文件

在这里插入图片描述

4、编译语言的发展

在这里插入图片描述

5、GCC 工作流程

在这里插入图片描述

GCC编译选项说明
-E预处理指定源文件,不进行编译
-S编译指定源文件,但是不进行汇编
-c编译、汇编指定的源代码,但是不进行链接
-o [file1] [file2] 或者
[file2] -o [file1]
将文件 file2 编译成可执行文件 file1
-I directory指定 include 包含文件的搜索目录
-g在编译的时候,生成调试信息,该程序可以被调试器调试
-D在程序编译的时候,指定一个宏
-W不生成任何警告信息

测试源代码编程汇编流程

1)、修改 test.c 文件,增加宏和注释的等信息

#include<stdio.h>
#define PI 3.14
int main()
{
    // 这是测试代码
    int sum = PI + 10;
    
    printf("hello world\n");
}

2)、预处理源文件,不进行编译

预处理 test.c 文件,并且指定生成 test.i 文件

gcc test.c -E -o test.i

查看预处理后的 test.i 文件,此时宏值已经被替换成真正的值,#incldue的头文件也自动将源文件插入当前文件中。

在这里插入图片描述

3)、 编译指定源文件,但是不进行汇编

编译预处理后的文件,生成汇编文件

gcc test.i -S -o test.s
或者
gcc test.i -S(默认生成 test.s 文件)

查看编译后的汇编代码 test.s

在这里插入图片描述

4)、汇编指定的源代码,但是不进行链接

将汇编代码通过汇编器转换为二进制代码

gcc test.s -c -o test.o

此时我们查看 test.o 发现已经不能查看。

在这里插入图片描述

5)、将二进制代码进行链接成可执行文件

gcc test.o -o test

此时我们可以直接执行 test

./test

测试跳过流程进行编译汇编

1)、跳过预处理流程

通过下面命令,我们可以跳过预处理流程,直接进行编译操作,生成汇编代码

gcc test.c -S

在这里插入图片描述

查看生成的汇编代码,没有变化

在这里插入图片描述

2)、跳过预处理、编译流程

执行下面命令可以跳过预处理、编译流程,直接生成可执行文件

gcc test.c

在这里插入图片描述

6、gcc 和 g++ 的区别

gcc 编译 C 程序,g++ 编译 C++ 程序。那么 g++ 能编译 C 程序吗?

是可以的,我们可以通过g++ test.c -o test 来编译汇编 C 程序,生成可执行文件。

在这里插入图片描述

  • gcc 和 g++ 都是 GNU(组织)的一个编译器
  • 误区一:gcc 只能编译 C 代码,g++ 只能编译 C++代码。两者都可以编译 C 代码,请注意:
    • 后缀为 .c 的,gcc 把它当作是 C 程序,而 g++ 当作是 C++ 程序
    • 后缀为 .cpp 的,两者都会认为是 C++ 程序,C++ 的语法规则更加严谨一些
    • 编译阶段,g++ 会调用 gcc,对于 C++ 代码,两者都是等价的,但是因为 gcc 命令不能自动和 C++ 程序使用的库链接,所以通常用 g++ 来完成链接,为了统一起见,干脆 编译/链接 统统用 g++ 了,这就给人一种错误,好像 cpp 程序只能用 g++ 似的。
  • 误区二:gcc 不会定义 _cplusplus 宏,而 g++ 会
    • 实际上,这个宏只是标志着编译器将会把代码按 C 还是 C++ 语法来解释
    • 如上所述,如果后缀为 .c,并且采用 gcc 编译器,则该宏就是未定义的,否则,就是已定义
  • 误区三:编译只能用 gcc,链接只能用 g++
    • 严格来说,这句话不算错误,但是它混淆了概念,应该这样说:编译可以用 gcc/g++,而链接可以用 g++ 或者 gcc -lstdc++ 。
    • gcc 命令不能自动和 C++ 程序使用的库链接,所以通常使用 g++ 来完成链接。但是在编译阶段,g++ 会自动调用 gcc,二者等价。

7、GCC 常用参数选项

GCC编译选项说明
-E预处理指定源文件,不进行编译
-S编译指定源文件,但是不进行汇编
-c编译、汇编指定的源代码,但是不进行链接
-o [file1] [file2] 或者
[file2] -o [file1]
将文件 file2 编译成可执行文件 file1
-I directory指定 include 包含文件的搜索目录
-g在编译的时候,生成调试信息,该程序可以被调试器调试
-D在程序编译的时候,指定一个宏
-W不生成任何警告信息
-Wall生成所有警告信息
-Onn的取值范围:0~3。编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高
-I在程序编译的时候,搜索的库的路径
-fPIC/fpic生成与位置无关的代码
-shared生成共享目标代码,通常用在建立共享库时
-std指定 C 方言,如:-std=c99,gcc 默认的方言是 GNU C

演示:-o [file1] [file2] 或者 [file2] -o [file1]

gcc test.c -o test
gcc -o app test.c

演示:-D 在程序编译的时候,指定一个宏

先创建一个 test.c 文件,内容如下:

#include<stdio.h>
int main()
{
    int a = 10;
#ifdef DEBUG
    printf("我是一个程序猿,我不会爬树...\n");
#endif
    for(int i = 0;i < 3; i++)
    {
        printf("hello,GCC!!!\n");
    }
    return 0;
}

使用 gcc 默认编译汇编链接后,生成执行文件,执行这个文件,可以发现并没有打印 printf("我是.......") 内容。

gcc -o test test.c
./test

在这里插入图片描述

再次使用 gcc 编译汇编链接,并加上 -DDEBUG 参数后,执行生成的程序,可以发现打印我们定义的输出语句

gcc -o app test.c -DDEBUG
./app

在这里插入图片描述

演示:-Wall 生成所有警告信息

gcc test.c -o test -Wall

在这里插入图片描述

演示:-On n的取值范围:0~3。编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高

#include<stdio.h>
int main()
{
    int b,c,d,f;
    b = 10;
    c = b;
    d = c;
    f = d;
    /*
    编译器优化后:
    int b,c,d,f;
    b = 10;
    c = 10;
    d = 10;
    f = 10;
    */
    return 0;
}

三、静态库的制作和使用

1、什么是库

  • 库文件是计算机上的一类文件,可以简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类。
  • 库是特殊的一种程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行。
  • 库文件有两种,静态库和动态库(共享库),区别是:
    • 静态库在程序的链接阶段被复制到了程序中;
    • 动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。
  • 库的好处
    • 代码保密
    • 方便部署和分发

2、命名规则

  • Linux:libxxx.a
    • lib:前缀(固定)
    • xxx:库的名字,自己命名即可
    • .a:后缀(固定)
  • Windows:libxxx.lib

3、静态库的制作

  • gcc 获得 .o 文件

  • 将多个 .o 文件打包,使用 ar 工具(archive)

    ar rcs libxxx.a xxx.o yyy.o zzz.o
    
    • r:将文件插入备存文件中
    • c:建立备存文件
    • s:索引

生成一个计算器的库

首先准备6个文件

在这里插入图片描述

add.c

#include <stdio.h>
#include "head.h"

int add(int a, int b)
{
    return a+b;
}

sub.c

#include <stdio.h>
#include "head.h"

int subtract(int a, int b)
{
    return a-b;
}

mult.c

#include <stdio.h>
#include "head.h"

int multiply(int a, int b)
{
    return a*b;
}

div.c

#include <stdio.h>
#include "head.h"

double divide(int a, int b)
{
    return (double)a/b;
}

head.h

#ifndef _HEAD_H
#define _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);

#endif

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 = %f\n", divide(a, b));
    return 0;
}

-C 编译、汇编指定的源代码,但是不进行链接

gcc -c add.c div.c mult.c sub.c

在这里插入图片描述

创建静态库

ar rcs libcalc.a add.o div.o mult.o sub.o

在这里插入图片描述

4、静态库的使用

创建 lesson05 文件夹,然后将 calc、library 文件夹复制到 lesson05 文件夹下

mkdir lesson05
cp -r calc/ library/ ../lesson05

回顾静态库的制作过程:

在这里插入图片描述

将生成的 libcalc.a 拷贝到 library/lib 目录下

cp /home/qykhhr/Linux/lesson05/calc/libcalc.a /home/qykhhr/Linux/lesson05/library/lib/

在这里插入图片描述

使用静态库需要知道静态库的头文件(./include/head.h),src 目录下是源文件,不参与编译过程,我们使用libcalc.a 静态库来实现加减乘除运算。

编译 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 = %f\n", divide(a, b));
    return 0;
}

直接将 main.c 进行编译

找不到 head.h 文件,没有指定头文件路径,默认是当前文件的目录下找head.h,没有找到这个文件。

gcc main.c -o app

在这里插入图片描述

-I 在程序编译的时候,搜索的库的路径

-I 路径

-I ./include/

指定搜索库的路径,指定在./include/(当前目录下的 include 目录下)head.h 文件。

但是没有指定使用哪个静态库

gcc main.c -o app -I ./include/

在这里插入图片描述

指定要加载的库

-l 库名称

-l calc(加载 libcalc.a 静态库)

指定要加载的库,但是找不到 这个库,没有指定库路径。

gcc main.c -o app -I ./include/ -l calc

在这里插入图片描述

指定要加载库的路径

-L路径

-L./lib

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

在这里插入图片描述

5、回顾整个静态库的制作与使用过程

在这里插入图片描述

1、静态库的制作

cd src/
gcc -c add.c sub.c div.c mult.c -I ../include/
ar rcs libsuanshu.a add.o sub.o div.o mult.o

在这里插入图片描述

2、静态库的使用

先将静态库复制到 lib 目录下

cp libsuanshu.a ../lib/
gcc -o main main.c -I ./include/ -l suanshu -L ./lib
./main

在这里插入图片描述

四、动态库(共享库)的制作和使用

1、动态库(共享库)的制作

1)、命名规则

  • Linux:libxxx.so(libxxx.so 库文件名称,xxx 库名称)
    • lib:前缀(固定)
    • xxx:库的名字,自己定义即可
    • .so:后缀(固定)
    • 在Linux下是一个可执行文件
  • Windows:libxxx.dll

2)、动态库(共享库)的制作

  • gcc 得到 .o 文件,得到和位置无关的代码

    gcc -c -fpic/-fPIC a.c b.c

    -fpic 用于编译阶段,产生的代码没有绝对地址,全部用相对地址,这正好满足了共享库的要求,共享库被加载时地址不是固定的。如果不加-fpic ,那么生成的代码就会与位置有关,当进程使用该.so文件时都需要重定位,且会产生成该文件的副本,每个副本都不同,不同点取决于该文件代码段与数据段所映射内存的位置。

  • gcc 得到动态库(共享库)

    gcc -shared a.o b.o -o libcalc.so

创建 lesson06 ,将上次代码复制到当前目录下

在这里插入图片描述

进入 calc 目录,执行下面命令生成汇编文件

cd calc
gcc -c -fpic add.c div.c mult.c sub.c

在这里插入图片描述

生成动态库(共享库)

gcc -shared add.o sub.o div.o mult.o -o libcalc.so

在这里插入图片描述

2、动态库的使用

将生成的动态库拷贝到我们需要使用的目录下的 lib 目录

cp ../calc/libcalc.so ./lib/

在这里插入图片描述

生成 main 程序的过程和静态库使用相同

在这里插入图片描述

但是在执行生成的程序时出现了错误:./main: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory

2、动态库加载失败的原因

动态库的工作原理

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

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

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

在这里插入图片描述

  • 如何定位共享库文件呢?

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

将动态库所在路径加入到 env 环境变量中

在这里插入图片描述

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/qykhhr/Linux/lesson06/library/lib
// 通过下面命令可以输出我们刚设置的环境变量值
echo $LD_LIBRARY_PATH

此时在执行ldd main命令,就可以看到依赖的动态库libcalc.so可以正常被找到了

在这里插入图片描述

执行程序

配置完环境变量后,我们就可以正常执行程序了

qykhhr@qykhhr:~/Linux/lesson06/library$ ./main
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667

但是新开一个窗口,程序又不能运行。我们需要将环境变量进行用户级别配置或者系统级别配置。

解决方式一:配置用户级别的环境变量

进入用户目录,编辑.bashrc

在这里插入图片描述

vim .bashrc
// shift + G 直接跳转到最后一行
// 在最后一行加入下面内容,然后保存退出
// source .bashrc 重新生效
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/qykhhr/Linux/lesson06/library/lib

在这里插入图片描述

source .bashrc 或者 source .bashrc 使修改的配置生效

此时再次执行ldd main就会找到 libcalc.so 动态库了,执行 main 程序也成功输出。

解决方式二:配置系统级别的环境变量

sudo vim /etc/profile
// 在最后一行加上
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/qykhhr/Linux/lesson06/library/lib

在这里插入图片描述

解决方式三:配置 /etc/ld.so.cache 文件

这个文件是二进制文件不能直接进行修改,我们需要修改 /etc/ld.so.conf 文件

sudo vim /etc/ld.so.conf
// 末尾加上下面内容
/home/qykhhr/Linux/lesson06/library/lib

// 更新
sudo ldconfig

在这里插入图片描述

解决方式四:将生成的动态库放在 /lib/,/usr/lib 目录下(不推荐)

将生成的动态库放进这两个目录,程序在使用动态库时就会自动加载这连个目录下的动态库文件。

但是不推荐这样使用,因为这两个目录下有系统使用的动态库,如果动态库名称冲突,就会有问题。

五、静态库和动态库的对比

1、程序编译成可执行程序的过程

在这里插入图片描述

2、静态库制作过程

在这里插入图片描述

3、动态库制作过程

在这里插入图片描述

4、静态库的优缺点

优点

静态库被打包到应用程序中加载速度快

发布程序无需提供静态库,移植方便

缺点

消耗系统资源,浪费内存

更新、部署、发布麻烦

在这里插入图片描述

5、动态库的优缺点

优点

可以实现进程间资源共享(共享库)

更新、部署、发布简单

可以控制何时加载动态库

缺点

加载速度比静态库慢

发布程序时需要提供依赖的动态库

六、Makefile

1、什么是 Makefile

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

redis的Makefile文件内容

在这里插入图片描述

2、Makefile 文件命名和规则

文件命名

makefile 或者 Makefile

Makefile 规则

  • 一个 Makefile 文件中可以有一个或者多个规则

    目标 ... : 依赖 ...
    	命令(Shell 命令)
    	...
    
    • 目标:最终要生成的文件(伪目标除外)
    • 依赖:生成的目标所需要的文件或是目标
    • 命令:通过执行命令对依赖操作生成目标(命令前必须 Tab 缩进)
  • Makefile 中的其他规则一般都是为第一条规则服务的。

测试 Makefile

在 lesson07 目录下创建 Makefile 文件,内容如下:

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

保存退出后,我们执行make,如果没有 make ,需要使用sudo apt install -y make 命令来安装 make 。

在这里插入图片描述

3、工作原理

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

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

测试:检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该规则中的命令

首先会检查 app:xxx.c xxx.c 中声明的对应的 .c 文件是否存在,如果存在就执行下面的 gcc 编译汇编命令。

我们修改 Makefile 文件,内容如下:

app:sub.o add.o mult.o div.o main.o
        gcc sub.o mult.o div.o add.o main.o -o app

sub.o:sub.c
        gcc -c sub.c -o  sub.o
add.o:add.c
        gcc -c add.c -o add.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

在执行第一行命令时,会发现没有 sub.o 等文件,于是会先向下执行,发现有生成 sub.o 等文件的命令,就会先执行生成 sub.o 等文件的命令,最后在执行生成 app 的命令。Makefile 中的其他规则一般都是为第一条规则服务的。Makefile 默认执行第一条规则,如果下面的规则和第一条规则没有关系就不会执行。

测试:如果下面的规则和第一条规则没有关系就不会执行

首先将 add.c 复制一份名为 b.c 文件

cp add.c b.c

然后修改 Makefile 文件内容

增加:
b.o:b.c
	gcc -c b.c -o b.o

删除上次生成的执行文件和所有 *.o 文件后,再次执行make

rm -rf *.o
make

会发现除了 b.o 文件没有生成,剩余的 .o 文件都会生成。

检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间

  • 如果依赖的时间比目标的时间晚,需要重新生成目标(即由 .c 文件生成的 .o 文件的时间比生成它的 .c 文件还早,说明此时 .c 文件已经被修改过,需要重新生成目标)
  • 如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被执行

执行一次make命令后,立刻再次执行make就会提示"xxx"已是最新

在这里插入图片描述

执行一次make命令后,修改 main.c 文件(加一个空行),再次执行make命令。

可以发现,gcc 只是将修改的 main.c 文件重新编译汇编,其他文件不会再次编译,最后再重新链接成一个新的可执行程序。所以我们第二次编写的 Makefile 更好。

在这里插入图片描述

4、变量

自定义变量

变量名=变量值

var=hello

获取我们定义的这个变量,使用 $(var) 即可获取变量值。

预定义变量

AR:归档维护程序的名称,默认值为 ar

CC:C 编译器的名称,默认值为 gcc

CXX:C++ 编译器的名称,默认值为 g++

$@:目标的完整名称

$<:第一个依赖文件的名称

$^:所有的依赖文件

$(CC) -c $^ -o $@

获取变量的值

$(变量名)

测试变量使用1

app:main.c a.c b.c
  gcc -c main.c a.c b.c

自动变量只能在规则的命令中使用

app:main.c a.c b.c
  $(CC) -c $^ -o $@
  • $^:代表 main.c a.c b.c
  • $@:代表要生成的可执行程序 app

测试变量使用2

使用变量前的 Makefile 文件内容:

app:sub.o add.o mult.o div.o main.o
        gcc sub.o mult.o div.o add.o main.o -o app

sub.o:sub.c
        gcc -c sub.c -o  sub.o
add.o:add.c
        gcc -c add.c -o add.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

使用变量后的 Makefile 文件内容:

# 定义变量
src=sub.o add.o mult.o div.o main.o
target=app
$(target):$(src)
        $(CC) $(src) -o $(target)
sub.o:sub.c
        gcc -c sub.c -o  sub.o
add.o:add.c
        gcc -c add.c -o add.o
b.o:b.c
        gcc -c b.c -o b.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

5、模式匹配

# 定义变量
src=sub.o add.o mult.o div.o main.o
target=app
$(target):$(src)
        $(CC) $(src) -o $(target)
sub.o:sub.c
        gcc -c sub.c -o  sub.o
add.o:add.c
        gcc -c add.c -o add.o
b.o:b.c
        gcc -c b.c -o b.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
# 定义变量
src=sub.o add.o mult.o div.o main.o
target=app
$(target):$(src)
        $(CC) $(src) -o $(target)

%.o:%.c
        $(CC) -c $< -o $@
  • %:通配符,匹配一个字符串
  • $<:代表第一个依赖文件的名称,即%.c 这个文件
  • $@:代表要生成的目标文件,即 %.o 这个文件

6、函数

$(wildcard PATTERN...)

  • 功能:获取指定目录下指定类型的文件列表

  • 参数:PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,一般用空格间隔

  • 返回:得到的若干个文件的文件列表,文件名之间使用空格间隔

  • 示例:

    $(wildcard *.c ./sub/*.c)

    返回值格式:a.c b.c c.c d.c e.c f.c

$(patsubst <pattern>,<replacement>,<text>)

  • 功能:查找<text>中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换。

  • <pattern>可以包括通配符'%',表示任意长度的字串。如果<replacement>中也包含'%',那么,<replacement>中的这个'%'将是<pattern>中的那个%所代表的字串。(可以用''来转义,以'%'来表示真实含义的'%'字符)

  • 返回:函数返回被替换过后的字符串

  • 示例:

    $(patsubst %.c,%.o,x.c bar.c)

    返回值格式:x.o bar.o

使用函数获取参数前的 Makefile 文件内容:

# 定义变量
src=sub.o add.o mult.o div.o main.o
target=app
$(target):$(src)
        $(CC) $(src) -o $(target)

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

使用函数获取参数后的 Makefile 文件内容:

# 获取当前目录下所有的.c文件
src=$(wildcard ./*.c)
# 返回src变量中的所有.c文件名,并且将返回的文件名后缀改为.o
objs=$(patsubst %.c,%.o, $(src))
target=app
$(target):$(objs)
        $(CC) $(objs) -o $(target)

%.o:%.c
        $(CC) -c $< -o $@
  • src=$(wildcard ./*.c):获取当前 Makefile 目录下的所有 .c 文件并赋值到 src 变量中
    • src=add.c mult.c div.c sub.c main.c
  • objs=$(patsubst %.c,%.o, $(src)):将所有输入的后缀为 .c 文件转为 .o 文件名并赋值给 objs 变量。
    • objs=add.o mult.o div.o sub.o main.o

最后我们是不需要生成的 .o 文件,我们可以在 Makefile 中声明在程序编译汇编链接生成后,将所有的 .o 文件删除的命令。

# 定义变量
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

执行完 make 后,我们在执行 make clean 既可以执行删除全部 .o 文件的命令

在这里插入图片描述

如果我们在 Makefile 目录下创建 clean 文件,再次执行 make clean 就会出现

在这里插入图片描述

原因是:clean在执行时没有源文件,所以每次执行make clean,目标文件都是最新的,都会执行这个规则下的命令,但是我们创建一个 clean 文件,这个 clean 文件默认就是clean这个目标的默认源文件。

clean:
        rm $(objs) -f

我们可以在 Makefile 中添加下面内容,来表示这个 clean 是一个伪目标,就不会生成特定文件,就不会和我们创建 clean 文件进行对比。

# 定义变量
src=$(wildcard ./*.c)
objs=$(patsubst %.c,%.o, $(src))
target=app
$(target):$(objs)
        $(CC) $(objs) -o $(target)

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

.PHONY:clean
clean:
        rm $(objs) -f

在这里插入图片描述

七、GDB 调试

1、什么是 GDB

GDB 是由 GNU 软件系统社区提供的调试工具,同 GCC 配套组成了一套完整的开发环境,GDB 是 Linux 和 许多类Unix 系统中的标准开发环境。

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

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

2、准备工作

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

gcc -g -Wall program.c -o program

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

如果生成 gdb 程序后,将源文件 test.c 移走,再次执行 GDB 相关操作就会出现找不到文件错误

gcc test.c -o test -g

在这里插入图片描述

在这里插入图片描述

3、GDB 命令 - 启动、退出、查看代码

gcc test.c -o test -g

在这里插入图片描述

启动和退出

gdb 可执行程序

quit

给程序设置参数/获取设置参数

set args 10 20

show args

GDB 使用帮助

help

查看当前文件代码

list/l(从默认位置显示源文件内容)

list/l 行号(从指定的行显示)

list/l 函数名(从指定的函数显示)

在这里插入图片描述

在这里插入图片描述

查看非当前文件代码

list/l 文件名:行号

list/l 文件名:函数名

需要加上行号或者函数名,否则就会将文件名当成函数名进行处理

g++ bubble.cpp main.cpp select.cpp -o main -g
gdb main

在这里插入图片描述

设置显示的行数

设置每次执行 list 后默认显示的条数(默认显示10条数据)

show list/listsize

set list/listsize 行数

4、GDB 命令 - 断点操作

设置断点

b/break 行号

b/break 函数名

b/break 文件名:行号

b/break 文件名:函数

查看断点

i/info b/break

在这里插入图片描述

删除断点

d/del/delete 断点编号

在这里插入图片描述

设置断点无效

dis/disable 断点编号

设置断点生效

ena/enable 断点编号

在这里插入图片描述

设置条件断点(一般用在循环的位置)

b/break 10 if i==5

在这里插入图片描述

在这里插入图片描述

5、GDB 命令 - 调试命令

运行 GDB 程序

start(程序停在第一行)

run(遇到断点才停)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

继续运行,到下一个断点停

c/continue

向下执行一行代码(不会进入函数体)

n/next

变量操作

p/print 变量名(打印变量值)

ptype 变量名(打印变量类型)

在这里插入图片描述

向下单步测试(遇到函数进入函数体)

s/step

finish(跳出函数体)

自动变量操作

display 变量名(自动打印指定变量的值)

i/info display

undisplay 编号

其他操作

set var 变量名=变量值(循环中用的较多)

until(跳出循环)

八、文件 IO(I:input,O:output)

1、标准 C 库 IO 函数

标准 C 库 IO 函数操作文件是可以跨平台的。

跨平台有几种实现方式,其中一种是 Java语言,Java语言跨平台的实现方式是:Java程序运行在 JVM 上,而对不同的系统设计不同的 JVM。另一种就是这种,通过调用标准库函数,在不同系统下调用不同系统的 API 去操作。

例如:我们使用标准 C 库 IO 函数fopen打开文件,在 Windows系统中,就会调用Windows系统的 API 打开文件,在 Linux 系统中就会调用 Linux 系统的 API 打开文件。

在这里插入图片描述

我们可以使用 Linux 自带的工具来查看 fopen 函数。

// 标准 C 库函数都在第3章
man 3 fopen

在这里插入图片描述

FILE.h中将 _IO_FILE 结构体再次声明为 FILE 结构体。

#ifndef __FILE_defined
#define __FILE_defined 1

struct _IO_FILE;

/* The opaque type of streams.  This is the definition used elsewhere.  */
typedef struct _IO_FILE FILE;

#endif

我们点击 _IO_FILE 来查看这个结构体内容

struct _IO_FILE {
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  // 文件读指针
  char* _IO_read_ptr;	/* Current read pointer */
  char* _IO_read_end;	/* End of get area. */
  char* _IO_read_base;	/* Start of putback+get area. */
  char* _IO_write_base;	/* Start of put area. */
  // 文件写指针
  char* _IO_write_ptr;	/* Current put pointer. */
  char* _IO_write_end;	/* End of put area. */
  // 缓冲区的起始指针
  char* _IO_buf_base;	/* Start of reserve area. */
  // 缓冲区的结束指针
  char* _IO_buf_end;	/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;
 
  // 文件描述符(整数值)
  int _fileno;
  // ......
};

2、标准 C 库 IO 和 Linux 系统 IO 的关系

在这里插入图片描述

3、虚拟地址空间

在这里插入图片描述

4、文件描述符

PCB:Process Control Block

在这里插入图片描述

5、Linux 系统 IO 函数

  • int open(const char *pathname, int flags);
  • int open(const char *pathname, int flags, mode_t mode);
  • int close(int fd);
  • ssize_t read(int fd, void *buf, size_t count);
  • ssize_t write(int fd, const void *buf, size_t count);
  • off_t lseek(int fd, off_t offset, int whence);
  • int stat(const char *pathname, struct stat *statbuf);
  • int lstat(const char *pathname, struct stat *statbuf);

1)、open 打开文件

查看Linux系统函数 - open()

Linux系统函数我们可以使用下面命令进行查看

// Linux系统IO函数在第二章
man 2 open

在这里插入图片描述

// 两个头文件包含了 open 函数参数中 flags 定义的宏 #include <sys/types.h> #include <sys/stat.h> // 包含了open函数的声明 #include <fcntl.h>

// 打开一个已经存在的文件 int open(const char *pathname, int flags); 参数:

  • pathname:要打开的文件路径
  • ags:对文件的操作权限设置还有其他的设置
    • Linux中可以通过 man 2 open 命令查看 open 函数相关信息,得到:The argument flags must include one of the following access modes: O_RDONLY, O_WRONLY, or O_RDWR. These request opening the file read-only, write-only, or read/write, respectively.
    • 必须要包含下面权限的一个:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(可读可写)

返回值:

  • open(), openat(), and creat() return the new file descriptor, or -1 if an error occurred (in which case, errno is set appropriately).
  • 返回一个新的文件描述符,如果调用失败,返回 -1,并将 errno 赋值为合适的值

errno:属于Linux系统函数库,库里面的一个全局变量,记录的是最近的错误号。

标准 C 库中定义了 perror() 函数用来打印 errno 对应的错误描述,我们可以通过 perror() 函数来打印错误信息。

#include <stdio.h>
void perror(const char *s);

参数:s

  • 用户描述,比如perror("hello"),最终输出的内容是 hello:xxx(xxx是实际的错误描述)

查看Linux系统函数 - close()

man 2 close

在这里插入图片描述

Linux系统函数在第二卷,我们可以使用 man 2 close 来查看 close() 函数的相关信息

#include <unistd.h>
int close(int fd);

我们调用 close 函数来关闭文件,需要传入一个文件描述符(file descriptor)。

调用 open 函数打开文件

/*
// 两个头文件包含了 open 函数参数中 flags 定义的宏
#include <sys/types.h>
#include <sys/stat.h>
// 包含了open函数的声明
#include <fcntl.h>

// 打开一个已经存在的文件
int open(const char *pathname, int flags);
    参数:
        - pathname:要打开的文件路径
        - flags:对文件的操作权限设置还有其他的设置
            - Linux中可以通过 man 2 open 命令查看 open 函数相关信息,得到:The argument flags must include one of the following access modes: O_RDONLY, O_WRONLY, or O_RDWR.  These request opening the file read-only, write-only, or read/write, respectively.
            - 必须要包含下面权限的一个:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(可读可写)
    返回值:
        - open(), openat(), and creat() return the new file descriptor, or -1 if an error occurred (in which case, errno is set appropriately).
        - 返回一个新的文件描述符,如果调用失败,返回 -1
errno:属于Linux系统函数库,库里面的一个全局变量,记录的是最近的错误号
标准 C 库中定义了 perror() 函数用来打印 errno 对应的错误描述
#include <stdio.h>
void perror(const char *s);
    参数:
        -s:用户描述,比如 hello,最终输出的内容是 hello:xxx(xxx是实际的错误描述)
// C 语言中是不允许重载的,这里是通过可变参数来实现重载效果的
// 创建一个新的文件
int open(const char *pathname, int flags, mode_t mode);

Linux系统函数在第二卷,我们可以使用 man 2 close 来查看 close() 函数的相关信息
#include <unistd.h>
int close(int fd);
*/ 
#include <sys/types.h>
#include <sys/stat.h>
// 包含了open函数的声明
#include <fcntl.h>
// 声明了 perror 函数
#include <stdio.h>
// 声明了 close 函数
#include <unistd.h>
int main()
{
    // 打开文件,返回一个 file descriptor(文件描述符)
    int fd = open("a.txt",O_RDONLY);
    if(fd == -1)
    {
        // 在没有 a.txt 这个文件时,perror函数就会打印:open函数: No such file or directory
        perror("open函数"); // open函数: No such file or directory
    }
    // 读写操作
 
    // 关闭文件,传入一个文件描述符(file descriptor)
    close(fd);
    return 0;
}

不存在 a.txt 文件时,perror函数就会打印错误信息。

在这里插入图片描述

2)、open 创建新文件

int open(const char *pathname, int flags, mode_t mode); 参数:

  • pathname:要创建的文件的路径

  • flags:对文件的操作权限和其他的设置

    • 必选项:O_RDONLY、O_WRONLY、O_RDWR 这三个之间是互斥的,只能选择一个

      • O_CREATE 文件不存在,创建新文件
      • O_APPEND 文件以附加模式打开
      • O_ASYNC 启用信号驱动I/O:当此文件描述符上的输入或输出成为可能时,生成一个信号(默认情况下为SIGIO
      • ......
    • flags 参数是一个int类型的数据,占4个字节,32位,每一位就是一个标志位。 我们传入参数时,使用按位或的方式添加权限或其他设置,相当于就是给标志位进行合并的操作( O_RDWR | O_CREAT )。

  • mode:八进制的数,表示创建出的新的文件的操作权限,比如:0775(0开头是八进制) 最终的文件操作权限是:mode & ~umask (按位与:0和任何数都为0)

    • mode 是我们传入的值

    • umask Linux控制台输入 umask 指令,就会打印当前用户的 umask 值 umask -- 0002,我们需要取反 ~umask = 0777 - 0002 = 0775

          0777  ---> 111111111
      &   0775  ---> 111111101
      -----------------------------
      		       111111101
      按位与:0和任何数都为0
      
    • umask的作用就是抹去某些权限,让我们设置权限更合理一些,我们可以通过 umask 022 设置 umask 值,也可以通过 mode_t umask(mode_t mask); 函数(man 2 umask)来在程序中指定 umask 值

-rwxrwxr-x 1 qykhhr qykhhr 0 9月 25 10:12 create.txt*

  • 第一个"-"符号文件的类型,"-"符号代表当前文件是一个普通文件,"d"符号代表当前文件是一个文件夹
  • 第一个字串 "rwx" 表示当前用户对这个文件的操作权限
    • 'r':表示当前用户这个文件有读取权限
    • 'w':表示当前用户对这个文件有写入权限
    • 'x';表示当前用户对这个文件有执行权限
  • 第二个字串 "rwx" 表示当前用户组中的用户对这个文件的操作权限
  • 第三个字串 "r_x" 表示其他用户组中的用户对这个文件的操作权限(没有了写入文件的权限)
/*
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags, mode_t mode);
    参数:
        - pathname:要创建的文件的路径
        - flags:对文件的操作权限和其他的设置
            - 必选项:O_RDONLY、O_WRONLY、O_RDWR 这三个之间是互斥的,只能选择一个
            - 可选项:
                - O_CREATE 文件不存在,创建新文件
                - O_APPEND 文件以附加模式打开
                - O_ASYNC 启用信号驱动I/O:当此文件描述符上的输入或输出成为可能时,生成一个信号(默认情况下为SIGIO
                - ......
            - flags 参数是一个int类型的数据,占4个字节,32位,每一位就是一个标志位。
                我们传入参数时,使用按位或的方式添加权限或其他设置,相当于就是给标志位进行合并的操作( O_RDWR | O_CREAT )。 
        - mode:八进制的数,表示创建出的新的文件的操作权限,比如:0775(0开头是八进制)
        最终的文件操作权限是:mode & ~umask 
            - mode 是我们传入的值
            - umask Linux控制台输入 umask 指令,就会打印当前用户的 umask 值
                umask -- 0002,我们需要取反 ~umask = 0777 - 0002 = 0775
            0777  ---> 111111111
        &   0775  ---> 111111101
      -----------------------------
                       111111101
      按位与:0和任何数都为0
      umask的作用就是抹去某些权限,让我们设置权限更合理一些,我们可以通过 umask 022 设置 umask 值,也可以通过 mode_t umask(mode_t mask); 函数(man 2 umask)来在程序中指定 umask 值

-rwxrwxr-x  1 qykhhr qykhhr    0 9月  25 10:12 create.txt*
    - 第一个"_"符号文件的类型
    - 第一个字串 "rwx" 表示当前用户对这个文件的操作权限
        - 'r':表示当前用户这个文件有读取权限
        - 'w':表示当前用户对这个文件有写入权限
        - 'x';表示当前用户对这个文件有执行权限
    - 第二个字串 "rwx" 表示当前用户组中的用户对这个文件的操作权限
    - 第三个字串 "r_x" 表示其他用户组中的用户对这个文件的操作权限(没有了写入文件的权限)
*/
// 包含 flags 参数的宏定义,O_RDONLY、ORDWR......
#include <sys/types.h>
#include <sys/stat.h>
// 包含 open 函数声明
#include <fcntl.h>
// 包含 perror 函数声明
#include<stdio.h>
// 包含 close 函数声明
#include <unistd.h>
int main()
{
    // -rwxrwxr-x  1 qykhhr qykhhr    0 9月  25 10:12 create.txt*
    // 生成的文件权限并不是我们设置的 0777 ,而是 0775,原因就是 mode & ~umask 
    int fd = open("create.txt",O_RDWR | O_CREAT,0777);
    if(fd == -1)
    {
        perror("open函数");
    }
    close(fd);
    return 0;
}

在这里插入图片描述

3)、read、write 函数

uni unix系统 unistd.h unix系统标准头文件

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

  • fd:文件描述符(file descriptor),通过 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 设置为合适的值
// read、write函数声明
#include <unistd.h>
// perror函数声明
#include<stdio.h>
// open 函数需要的头文件
// 定义一些读写权限的宏
#include <sys/types.h>
#include <sys/stat.h>
// open函数的声明
#include <fcntl.h>
/*
// uni unix系统
// unistd.h unix系统标准头文件
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
    参数:
        - fd:文件描述符(file descriptor),通过 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 设置为合适的值
*/
// read、write函数声明
#include <unistd.h>
// perror函数声明
#include<stdio.h>
// open 函数需要的头文件
// 定义一些读写权限的宏
#include <sys/types.h>
#include <sys/stat.h>
// open函数的声明
#include <fcntl.h>

int main()
{
    // 1、通过 open 打开 english.txt 文件
    int srcfd = open("english.txt",O_RDONLY);
    if(srcfd == -1)
    {
        perror("open函数错误");
        return -1;
    }
    // 2、创建一个新的文件(拷贝文件)
    int destfd = open("copy.txt",O_WRONLY | O_CREAT,0664);
    if(destfd == -1)
    {
        perror("open函数错误");
        return -1;
    }
    // 3、频繁的读写操作
    char buf[1024] = {0};
    int len = 0;
    // len 是读取文件后返回的实际buf中的字节数
    while((len = read(srcfd,buf,sizeof(buf))) > 0)
    {
        // 向destfd文件描述符指向的文件写入buf中的数据,buf实际字节数是len长度
        write(destfd,buf,len);
    }
    // 4、关闭文件
    close(srcfd);
    close(destfd);
    return 0;
}

在这里插入图片描述

4)、lseek 函数

Linux系统函数中的 lseek 函数(man 2 lseek) #include <sys/types.h> #include <unistd.h> off_t lseek(int fd, off_t offset, int whence); 参数:

  • fd:文件描述符,通过 open 函数得到的,通过这个 fd 操作某个文件
  • offset:偏移量
  • whence:
    • SEEK_SET:设置文件指针的偏移量
    • SEEK_CUR:设置偏移量:当前位置 + 第二个参数 offset 的值
    • SEEK_END:设置偏移量:文件末尾 + 第二个参数 offset 的值

返回值:返回文件指针的位置

lseek函数的作用:

  1. 移动文件指针到文件头

    lseek(fd, 0, SEEK_SET);

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

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

  4. 拓展文件的长度,当前文件 10b,lseek(fd, 100, SEEK_END),增加了 100 个字节 lseek(fd, 100, SEEK_END);

平时在下载文件时,会先创建临时文件,占用实际的大小空间,下载过程中再依次替换成自己的数据,防止下载过程中空间不足。

/*
Linux系统函数中的 lseek 函数(man 2 lseek)
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
    参数:
        - fd:文件描述符,通过 open 函数得到的,通过这个 fd 操作某个文件
        - offset:偏移量
        - whence:
            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,lseek(fd, 100, SEEK_END),增加了 100 个字节
            lseek(fd, 100, SEEK_END);
            平时在下载文件时,会先创建临时文件,占用实际的大小空间,下载过程中再依次替换成自己的数据,防止下载过程中空间不足。
标准 C 库中的 lseek 函数(man 3 fseek)
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
*/
// lseek 函数声明
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<stdio.h>
int main()
{
    int fd = open("hello.txt",O_RDWR);
    if(fd == -1)
    {
        perror("open函数");
        return -1;
    }
    // 拓展文件长度
    int ret = lseek(fd,100,SEEK_END);
    if(ret == -1)
    {
        perror("lseek函数");
        return -1;
    }
    // 扩展完后,我们需要写入数据,否则文件大小还是原来大小
    // 写入一个空字符串
    write(fd," ",1);
    close(fd);
    return 0;
}

我们先使用 lseek 函数将 11字节的数据偏移了 100B 大小,但是文件大小没有改变,我们需要在偏移后使用 write 函数写入数据,此时偏移后的文件大小为 112B 大小,多出 1字节 大小,是因为我们写入了一个空字符。

在这里插入图片描述

5)、stat、lstat 函数

stat 函数

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

int stat(const char *pathname, struct stat *statbuf); 作用:获取一个文件相关的一些信息 参数:

  • pathname:操作的文件的路径
  • statbuf:结构体变量,传出参数,用于保存获取到的文件的信息

返回值:

  • 成功:返回 0
  • 失败:返回 -1,并设置合适的 errno

int lstat(const char *pathname, struct stat *statbuf); 参数:

  • pathname:操作的文件的路径
  • statbuf:结构体变量,传出参数,用于保存获取到的文件的信息

返回值:

  • 成功:返回 0
  • 失败:返回 -1,并设置合适的 errno
/*
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *statbuf);
    作用:获取一个文件相关的一些信息
    参数:
        - pathname:操作的文件的路径
        - statbuf:结构体变量,传出参数,用于保存获取到的文件的信息
    返回值:
        成功:返回 0
        失败:返回 -1,并设置合适的 errno
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
    参数:
        - pathname:操作的文件的路径
        - statbuf:结构体变量,传出参数,用于保存获取到的文件的信息
    返回值:
        成功:返回 0
        失败:返回 -1,并设置合适的 errno 
*/
#include<sys/stat.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
    // 通过 stat 函数获取文件信息,进而获取文件大小信息
    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;
}

在这里插入图片描述

lstat 函数

我们使用命令给 a.txt 文件创建一个软链接,名字为 b.txt

ln -s a.txt b.txt

在这里插入图片描述

lstat 函数的作用就是获取软链接文件的信息。

在这里插入图片描述

6、stat 结构体

struct stat {
    dev_t st_dev; // 文件的设备编号
    ino_t st_ino; // 节点
    mode_t st_mode; // 文件的类型和存取的权限
    nlink_t st_nlink; // 连到该文件的硬连接数目
    uid_t st_uid; // 用户 ID
    gid_t st_gid; // 组 ID
    dev_t st_rdev; // 设备文件的设备编号
    off_t st_size; // 文件字节数 文件大小
    blksize_t st_blksize; // 块大小
    blkcnt_t st_blocks; // 块数
    time_t st_atime; // 最后一次访问时间
    time_t st_mtime; // 最后一次修改时间
    time_t st_ctime; // 最后一次改变时间 指属性
};

7、st_mode 变量(文件的类型和存取的权限)

在这里插入图片描述

查看当前文件的类型

S_IFMT 0170000

在这里插入图片描述

文件类型和掩码 S_IFMT 进行按位与操作后,后面的位信息全部变为0,文件类型对应4位数据没变,然后再比较前四位的值和8种文件类型的对应的数值进行比较,从而达到判断这个文件类型的目的。

判断一个文件是否有其他组执行的权限

S_IXOTH 00007

在这里插入图片描述

我们需要将这个16位的数据和 S_IXOTH 进行按位与操作,如果有执行权限,最后一位就不为 0。

模拟实现 ls -l 命令

#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
// 声明了 struct passwd *getpwuid(uid_t uid); 函数,通过 uid 获取用户名
#include <pwd.h>
// 声明了 struct group *getgrgid(gid_t gid); 函数,通过 gid 获取组名称
#include <grp.h>

// ctime() 函数声明
#include<time.h>
// strncpy() 函数声明
#include<string.h>
// 模拟实现 ls -l 指令
// -rw-rw-r-- 1 qykhhr qykhhr 12 9月  25 14:02 a.txt
int main(int argc,char * argv[])
{
    printf("%d\n",argc);
    // int len = sizeof(*argv);
    // for(int i = 0; i < len; i++)
    // {
    //     printf("%s\n",argv[i]);
    // }   

    // 判断输入的参数是否正确
    if(argc < 2)
    {
        printf("%s filename\n",argv[0]);
        return -1;
    }
    // int stat(const char *pathname, struct stat *statbuf);
    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_IFLNK:
        perms[0] = 'l';
        break;
    case __S_IFDIR:
        perms[0] = 'd';
        break;
    case __S_IFREG:
        perms[0] = '-';
        break;
    case __S_IFBLK:
        perms[0] = 'b';
        break;
    case __S_IFCHR:
        perms[0] = 'c';
        break;
    case __S_IFSOCK:
        perms[0] = 'p';
        break;
   default:
        perms[0] = '?';
        break;
    }
    // 判断文件的访问权限
    // 判断文件所有者权限
    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 * filelGrp = getgrgid(st.st_gid)->gr_name;

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

    // 获取修改时间
    char * time = ctime(&st.st_mtime);
    // 生成的 time 默认会有一个换行,我们需要去除
    char mtime[512] = {0};
    strncpy(mtime,time,strlen(time) - 1);

    // 输出
    char buf[1024];
    sprintf(buf,"%s %d %s %s %ld %s %s",perms,linkNum,fileUser,filelGrp,fileSize,mtime,argv[1]);
    printf("%s\n",buf);
    return 0;
}

在这里插入图片描述

8、文件属性操作函数

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

#include <unistd.h> int access(const char *pathname, int mode); 作用: 判断某个文件是否有某个权限,或者判断文件是否存在 参数:

  • pathname:判断的文件路径
  • mode:
    • R_OK:判断是否有读权限
    • W_OK:判断是否有写权限
    • X_OK:判断是否有执行权限
    • F_OK:判断文件是否存在

返回值:

  • 成功:返回 0
  • 失败:返回 -1,并将 errno 设置为合适的值
/*
#include <unistd.h>
int access(const char *pathname, int mode);
    作用: 判断某个文件是否有某个权限,或者判断文件是否存在
    参数:
        - pathname:判断的文件路径
        - mode:
            R_OK:判断是否有读权限
            W_OK:判断是否有写权限
            X_OK:判断是否有执行权限
            F_OK:判断文件是否存在
    返回值:
        成功:返回 0
        失败:返回 -1,并将 errno 设置为合适的值
*/
#include <unistd.h>
#include <stdio.h>
int main()
{
    int ret = access("a.txt",F_OK);
    if(ret == -1)
    {
        perror("access函数");
        return -1;
    }
    printf("文件存在!\n");
    return 0;
}

在这里插入图片描述

int chmod(const char *filename,int mode);

#include <sys/stat.h> int chmod(const char *pathname, mode_t mode); 作用:修改文件的权限 参数:

  • pathname:需要修改的文件的路径
  • mode:需要修改的权限值

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

/*
 #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",0755);
    if(ret == -1)
    {
        perror("chmod函数");
        return -1; 
    }

    return 0;
}

在这里插入图片描述

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

作用:修改文件的所在组

查看 Linux下用户相关信息

vim /etc/passwd

在这里插入图片描述

查看用户组信息

vim /etc/group

在这里插入图片描述

添加用户,查看用户组信息

sudo useradd hhr
id hhr

在这里插入图片描述

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

#include <unistd.h> #include <sys/types.h> int truncate(const char *path, off_t length); 作用:缩减或者扩展文件的尺寸至指定的大小 参数:

  • path:需要修改的文件的路径
  • length:需要最终文件变成的大小

将文件由12字节扩展到20字节

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

在这里插入图片描述

b.txt 文件内容也由"hello,world",变成了下面内容

在这里插入图片描述

将文件由20字节缩减到5字节

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

int main()
{
    int ret = truncate("b.txt",5);
    if(ret == -1)
    {
        perror("truncate函数");
        return -1;
    }
    return 0;
} 

在这里插入图片描述

9、目标操作函数

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

#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>
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);

#include <stdio.h> int rename(const char *oldpath, const char *newpath); 作用:重命名文件/文件夹 参数:

  • oldpath 修改前的路径
  • newpath 修改后的路径
/*
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
    作用:重命名文件/文件夹
    参数:
        oldpath 修改前的路径
        newpath 修改后的路径
*/
#include <stdio.h>
int main()
{
    int ret = rename("aaa","bbb");
    if(ret == -1)
    {
        perror("rename函数");
        return -1;
    }

    return 0;
}

在这里插入图片描述

int chdir(const char *path);

#include <unistd.h> int chdir(const char *path); 作用:修改进程的工作目录。比如在 /home/qykhhr 启动了一个可执行程序a.out,进程的工作目录就是 /home/qykhhr 参数:

  • path:需要修改的工作目录

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

#include <unistd.h> // cwd(current work directory,当前工作路径) char *getcwd(char *buf, size_t size); 作用:获取当前工作目录 参数:

  • buf:存储的路径,指向的是一个数组(传出参数)
  • size:数组的大小

返回值:返回的指向的一块内存,这个数据就是第一个参数

获取当前的工作目录,修改当前工作目录,并创建一个新文件

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

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

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

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

    // 修改工作目录
    int ret = chdir("/home/qykhhr/Linux/lesson13");
    if(ret == -1)
    {
        perror("chdir函数");
        return -1;
    }
    // 创建一个新的文件
    int fd = open("chdir.txt",O_CREAT|O_RDWR,0644);
    if(fd == -1)
    {
        perror("open函数");
        return -1;
    }
    close(fd);

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

    return 0;
}

在这里插入图片描述

10、目录遍历函数

DIR* opendir(const char *name);

#include <sys/types.h> #include <dirent.h> DIR *opendir(const char *name); 可以通过(man 3 opendir)命令来查看这个指令的用法 作用:打开一个目录 参数:

  • name:需要打开的目录的名称

返回值

  • DIR * 类型,可以理解为目录流
  • 错误返回 NULL

struct dirent *readdir(DIR *dirp);

#include <dirent.h> struct dirent *readdir(DIR *dirp); 作用:读取目录中的数据 参数:

  • dirp:是 opendir 函数返回的结果

返回值:

  • struct dirent 代表读取到的文件的信息
  • 读取到了末尾或者失败了,返回 NULL

int closedir(DIR* dirp);

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

读取某个目录下所有的额普通文件的个数

/*
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name); 可以通过(man 3 opendir)命令来查看这个指令的用法
    作用:打开一个目录
    参数:
        name:需要打开的目录的名称
    返回值
        DIR  * 类型,可以理解为目录流
        错误返回 NULL

#include <dirent.h>
struct dirent *readdir(DIR *dirp);
    作用:读取目录中的数据
    参数:
        dirp:是 opendir 函数返回的结果
    返回值:
        struct dirent 代表读取到的文件的信息
        读取到了末尾或者失败了,返回 NULL

#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
    作用:关闭目录
*/
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.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("普通文件个数为:%d\n",num);
    return 0;
}

// 用于获取目录下所有普通文件的个数
int getFileNum(const char * path)
{
    // 1、打开目录
    DIR * dir = opendir(path);
    if(dir == NULL)
    {
        perror("opendir函数");
        exit(0);
    }
    // 普通文件的数量
    int total = 0;
    struct dirent* ptr;
    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[1024];
            sprintf(newpath,"%s/%s",path,ptr->d_name);
            total += getFileNum(newpath);
        }
        if(ptr->d_type == DT_REG)
        {
            // 普通文件
            total++;
        }
    }
    // 关闭目录
    closedir(dir);
    return total;
}

在这里插入图片描述

11、dirent 结构体和 d_type

struct dirent
{
    // 此目录进入点的 inode
    ino_t d_ino;
    // 目录文件开头至此目录进入点的位移
    off_t d_off;
    // d_name   的长度 , 不包含 NULL 字符
    unsigned short int d_reclen;
    // d_name  所指的文件类型
    unsigned char d_type;
    // 文件名
    char d_name[256];
};

d_type

  • DT_BLK - 块设备
  • DT_CHR - 字符设备
  • DT_DIR - 目录
  • DT_LNK - 软连接
  • DT_FIFO - 管道
  • DT_REG - 普通文件
  • DT_SOCK - 套接字
  • DT_UNKNOWN - 未知

12、dup、dup2 函数

int dup(int oldfd);

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

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

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

int main()
{
    int fd = open("a.txt",O_CREAT | O_RDWR,0777);
    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);

#include <unistd.h> int dup2(int oldfd, int newfd); 作用:重定向文件描述符 oldfd 指向 a.txt,newfd 指向 b.txt 调用函数成功后:newfd 和 b.txt 没有关联,newfd 指向了 a.txt oldfd 必须是一个有效的文件描述符 oldfd 和 newfd 值相同,相当于什么都没做

/*
#include <unistd.h>
int dup2(int oldfd, int newfd);
    作用:重定向文件描述符
    oldfd 指向 a.txt,newfd 指向 b.txt
    调用函数成功后:newfd 和 b.txt 没有关联,newfd 指向了 a.txt
    oldfd 必须是一个有效的文件描述符
    oldfd 和 newfd 值相同,相当于什么都没做
*/
#include <unistd.h>

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

#include <stdio.h>
#include <string.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("fd = %d, fd1 = %d\n",fd,fd1);

    int fd2 = dup2(fd,fd1);// 返回的fd2和fd1值相同
    if(fd2 == -1)
    {
        perror("dup2函数");
        return -1;
    }
    // 通过 fd1 操作数据,实际操作是1.txt,而不是2.txt
    char * str = "hello,world\n";
    
    int len = write(fd1,str,strlen(str));
    if(len == -1)
    {
        perror("write函数");
        return -1;
    } 
    printf("fd = %d, fd1 = %d, fd2 = %d\n",fd,fd1,fd2);  

    close(fd);
    // 因为 fd1 和 fd2 相同,所有我们只需要关闭一个即可
    close(fd1);
    return 0;
}

在这里插入图片描述

13、fcntl 函数

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

  • 复制文件描述符,
  • 设置/获取文件标志

#include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd, ... ); 参数:

  • fd:表示需要操作的文件描述符
  • cmd:表示对文件描述符进行如何操作
    • F_DUPFD:复制文件描述符,复制到第一个参数fd,得到一个新的文件描述符(返回值) int ret = fcntl(fd,F_DUPFD)l
    • F_GETFL:获取指定的文件描述符文件状态flag 获取的flag和我们通过open函数传递的flag是一个东西
    • F_SETFL:设置文件描述符文件状态flag 必选项:O_RDONLY,O_WRONLY,O_RDWR 不可以被修改 可选项:O_APPEND。O_ASYNC,O_NONBLOCK O_APPEND 表示追加数据 O_NONBLOCK 设置成非阻塞

阻塞和非阻塞:描述的是函数调用的行为。

/*
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... );
    参数:
        fd:表示需要操作的文件描述符
        cmd:表示对文件描述符进行如何操作
            - F_DUPFD:复制文件描述符,复制到第一个参数fd,得到一个新的文件描述符(返回值)
                int ret = fcntl(fd,F_DUPFD)l
            - F_GETFL:获取指定的文件描述符文件状态flag
                获取的flag和我们通过open函数传递的flag是一个东西
            - F_SETFL:设置文件描述符文件状态flag
                必选项:O_RDONLY,O_WRONLY,O_RDWR 不可以被修改
                可选项:O_APPEND。O_ASYNC,O_NONBLOCK
                    O_APPEND 表示追加数据
                    O_NONBLOCK 设置成非阻塞
        阻塞和非阻塞:描述的是函数调用的行为。 

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

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

    // 2、修改或者获取文件状态 flag
    int fd = open("1.txt",O_RDWR);
    if(fd == -1)
    {
        perror("open函数");
        return -1;
    }
    // 获取文件描述符状态 flag
    // 直接加上O_APPEND这个参数会丢失之前的文件状态,我们需要先获取文件状态然后和O_APPEND进行按位或
    int flag = fcntl(fd, F_GETFL);
    if(flag == -1)
    {
        perror("fcntl函数");
        return -1;
    }
    flag |= O_APPEND; // flag = falg | O_APPEND;
    // 修改文件描述符的flag,给flag加入O_APPEND 这个标记,直接加上O_APPEND这个参数会丢失之前的文件状态,我们需要先获取文件状态然后和O_APPEND进行按位或
    int ret = fcntl(fd,F_SETFL,flag);
    if(ret == -1)
    {
        perror("fcntl函数");
        return -1;
    }
    char * str = "world\n";
    int len = write(fd,str,strlen(str));
    if(len == -1)
    {
        perror("write函数");
        return -1;
    }
    close(fd);
    close(ret);
    return 0;
}

在这里插入图片描述

end

评论