Linux开发基础(1):Makefile、GDB、文件IO及其各种操作函数

1.5 Makefile

01 什么是Makefile

  • 一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,Makefile文件定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为Makefile文件就像一个Shell脚本一样,也可以执行操作系统的命令。

  • Makefile带来的好处就是“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是一个解释Makefile 文件中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如Delphi的make,Visual C++的nmake, Linux下GNU的make。

02 Makefile文件命名和规则

  • 文件命名
    makefile或者Makefile
  • Makefile规则
    一个Makefile文件中可以有一个或者多个规则
目标 ... : 依赖 ...
    命令(shell命令)
    ...

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

03 工作原理

  • 令在执行之前,需要先检查规则中的依赖是否存在
    如果存在,执行命令
    如果不存在,向下检查其它的规则,检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该规则中的命令
  • 检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间
    如果依赖的时间比目标的时间晚,需要重新生成目标
    如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被执行

04变量

  • 自定义变量
    变量名=变量值,例:var=hello
  • 预定义变量
    AR:归档维护程序的名称,默认值为ar
    CC:C编译器的名称,默认值为CC
    CXX:C++编译器的名称,默认值为g++
    $@:目标的完整名称
    $<:第一个依赖文件的名称.
    $^:所有的依赖文件
  • 获取变量的值
    $(变量名)
  • 示例
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 $@

1.6 GDB调试

01 什么是GDB

  • GDB是由GNU软件系统社区提供的调试工具,同GCC配套组成了一套完整的开发环境,GDB是Linux和许多类Unix系统中的标准开发环境。
  • 一般来说,GDB主要帮助你完成下面四个方面的功能:
  1. 启动程序,可以按照自定义的要求随心所欲的运行程序
  2. 可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式)
  3. 当程序被停住时,可以检查此时程序中所发生的事
  4. 可以改变程序,将一个BUG产生的影响修正从而测试其他BUG

02 准备工作

  • 通常,在为调试而编译时,我们会关掉编译器的优化选项(-O) ,并打开调试选项(-g) 。另外,-Wall在尽量不影响程序行为的情况下选项打开所有warning,也可以发现许多问题,避免一些不必要的BUG。
  • gcc -g -Wall program.c -o program
  • -g 选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证gdb能找到源文件。

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

  • 启动和退出
    gdb 可执行程序
    quit
  • 给程序设置参数/获取设置参数
    set args 参数(多个参数用空格隔开)
    show args
  • GDB使用帮助
    help
  • 查看当前文件代码
    list/l (从默认位置显示)
    list/l 行号 (从指定的行显示)
    list/l 函数名 (从指定的函数显示)
  • 查看非当前文件代码
    list/l 文件名:行号
    list/l 文件名:函数名
  • 设置显示的行数
    show list/listsize
    set list/listsize 行数

04 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

05 GDB命令—调试命令

  • 运行GDB程序
    start (程序停在第一行)
    run (遇到断点才停)
  • 继续运行,到下一个断点停
    c/continue
  • 向下执行一行代码(不会进入函数体)
    n/next
  • 变量操作
    p/print 变量名 (打印变量值)
    ptype 变量名 (打印变量类型)
  • 向下单步调试(遇到函数进入函数体)
    s/step
    finish (跳出函数体)
  • 自动变量操作
    display 变量名 (自动打印指定变量的值)
    i/info display
    undisplay 编号
  • 其它操作
    set var 变量名=值 (设置变量的值)
    until (跳出循环)

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

01 标准C库IO函数

  • 上图的一些说明:调用fopen后返回一个FILE指针,指针指向的结构体内有三个内容:文件描述符(指向文件,用于定位文件),文件读写指针(多个指针分别用于读和写),I/O缓冲区(缓冲区减少与磁盘打交道的次数,增加了效率,上图最右侧的小字代表往从缓冲区磁盘写数据的三种情况)。

  • 差别1:标准C库IO函数可以实现跨平台。跨平台就是开发的程序可以在任何操作系统上运行,所谓的跨平台就是跨操作系统。
    跨平台实现方式:1.例如java,程序运行在Java虚拟机上。2.第三方库例如标准C库,IO函数调用操作系统提供的api。

  • 差别2:标准C库IO函数带有缓冲区,缓冲区减少了写入磁盘的次数,增加了效率

02 标准C库IO和Linux系统IO的关系


标准C库IO和Linux系统IO是调用和被调用的关系。write和read是Linux系统的IO,没有缓冲区。写数据时,C库IO函数fputs多次写入缓冲区,缓冲区满(或刷新缓冲区或正常关闭文件)后调用一次write写入磁盘。读数据时,C库IO函数fgets调用一次read从磁盘写满缓冲区,然后多次从缓冲区读数据。

03 虚拟地址空间

  • 虚拟地址空间不是真正存在的,32位操作系统232,也就是4G,64位操作系统是248。上图是32位操作系统的虚拟地址空间。虚拟地址空间由MMU映射到真实的物理地址。
  • 上图的一些说明:图中从下到上,是从低地址到高地址。0~3G是用户区(3个G),3~4G是内核区(1个G)。NULL和nullptr储存在受保护的地址。堆空间存储时是由低地址到高地址,栈空间存储时是从高地址到低地址。堆空间比较大,栈空间比较小。普通用户对内核区没有操作权限,但是可以通过调用系统API(如write、read)完 成某些操作。

04 文件描述符

  • 上图的一些说明:文件描述符由PCB进程控制块维护,由数组形式(文件描述符表)存放在PCB这个大的结构体中。文件描述符表的大小是1024,也就是说一个进程最多打开1024个文件。文件描述符表前面0、1、2三个位置被占用,分别是标准输入、标准输出、标准错误,它们三个都指向当前终端(linux中一切皆为文件)。每打开一个新文件,则占用一个文件描述符,而且是空闲的最小的一个文件描述符。当一个进程多次打开相同的文件时,会占用多个文件描述符,每个文件描述符表不相同。

05 Linux系统IO函数

open 打开已经存在的文件

int open (const char *pathname, int flags);     
/* 
打开一个已经存在的文件。指令 man 2 open 查看详情。
头文件:#include <sys/types.h>  
       #include <sys/stat.h>  
       #include <fcntl.h>(open函数的声明实际上是在fcntl.h里,但是下面要说的参数flags的值是定义的宏,分别存放在上面两个头文件中)
pathname:要打开的文件路径。
flags:参数flags是通过 O_RDONLY,O_WRONLY 或 O_RDWR (指明文件是以 只读,只写 或 读写 方式打开的)与零个或多个可选模式做 | 操作得到的。 O_RDONLY,O_WRONLY 或 O_RDWR必须选择一个,而 | 后面的可选模式可以选择也可以不选择。可选模式常用的有:O_APPEND、O_TRUNC、O_LARGEFILE、O_CREAT(如果选择O_CREAT则代表此时要创建一个新文件,需要open后面多一个参数mode决定新创建文件的权限)等,可选模式的详细功能利用指令 man 2 open 查看。
返回值:文件描述符,如果打开失败则返回-1。
errno:属于Linux系统函数库,库里面的一个全局变量,记录的是最近的错误号。当系统调用(system call)产生错误后,errno会被附上合适的错误号(每种错误号代表不同的错误种类)。指令 man 3 errno 查看详情。
*/

void perror(const char *s);
/* 
打印errno对应的错误描述,指令 man 3 perror 查看详情。
头文件:#include <stdio.h>
s:用户描述,可以是任意的,比如hello,最终输出的内容是 hello : xxx(实际的错误描述,例如No such file or directory)
*/
  • 示例
    编写代码,只读方式下打开同一级目录下的“a.txt”文件,如果打开失败,则利用perror打印错误描述

    编译并执行,可以看到,由于不存在“a.txt”文件,所以打印了错误信息“No such file or directory”

    创建“a.txt”文件,再次执行,没有报错

open创建一个新的文件

int open(const char *pathname, int flags, mode_t mode);    
/* 
创建一个新的文件。指令 man 2 open 查看详情。
头文件:见上文“open 打开已经存在的文件”。
pathname:要创建的新文件的路径。
flags:见上文“open 打开已经存在的文件”。此时必须选择O_CREAT
mode:mode只有当在flags中使用O_CREAT时才有效,否则被忽略,它是一个八进制的数,表示创建的新文件的操作权限。例如:0777,第一个7代表创建文件的用户可读可写可执行,第二个7代表同组可读可写可执行,第三个7代表其他组可读可写可执行。但是最终文件的权限不一定是0777,最终的权限是: mode & (~umask)。
umask:用于抹去某些权限。不同用户的umask有不同的默认值,指令 umask 可以查看当前用户的umask值,root的umask值默认是0022。可以通过指令[umask 数值]改变当前用户的umask值,但是这种改变只是在当前终端上的改变,启动另一个终端登录该用户还是默认的umask值。假设mode=0777,当前用户是root用户,那么最终权限就是 0777 & (~0022),最后的权限结果是0755(计算过程先变成二进制再计算),也就是创建文件的用户可读可写可执行,同组可读不可写可执行,其他组可读不可写可执行。后面会用示例演示一遍
返回值:文件描述符,如果打开失败则返回-1。
*/
  • 示例
    编写代码,在同一级目录下创建“a.txt”文件,mode=0777,如果失败,则利用perror打印错误描述

    当前用户root,umask=0022,当前目录下只有creat.c文件,编译并执行,可以看到创建了a.txt文件,且权限是rwxr-xr-x

  • 为什么flags的多个选项采用用 | 操作。因为flags是int类型的值,二进制共32位,某些位置代表读、写或其他选项,这些位上如果是1则代表有,0代表没有, | 操作后把所有的1都保留下来,就可以实现多种选项同时选择。

read

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

write

ssize_t write(int fd, const void *buf, size_t count);
/*
写文件,指令 man 2 write 查看详情
头文件:#include <unistd.h>
fd:文件描述符,open得到的,通过这个文件描述符操作某个文件
buf:要往磁盘写入的数据
count:要写的数据的实际的大小
*/
示例

编写程序,将test.txt文件中的内容拷贝一份到新文件copytest.txt中。

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

int main()
{
    int srcfd = open("test.txt", O_RDONLY);   //只读方式下打开原文件
    int copyfd = open("copytest.txt", O_WRONLY | O_CREAT, 0664);  //只写方式下创建拷贝文件,并设置权限
    char buf[1024] = {0};   //创建缓冲数组
    int len = 0;   
    
    //循环写入数据,len等于0代表读完了,或者len小于0代表读取中断,两种情况跳出循环
    while((len = read(srcfd, buf, sizeof(buf))) > 0)
    {
        write(copyfd, buf, len);  //注意这里最后一个参数是len,指实际要写入的大小,而不是buf的大小
    }

    //关闭文件
    close(srcfd);  
    close(copyfd);

    return 0;
}

编译并运行,可以看到拷贝后的两个文件大小一致

lseek

off_t lseek(int fd, off_t offset, int whence);
/*
重新定位文件读写的位移,指令 man 2 lseek 查看详情
头文件:#include <sys/types.h>   #include <unistd.h>
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, 扩展成110b, 增加了100个字节。 lseek(fd, 100,SEEK_END);
*/
示例

编写程序,11字节的文件helloworld,扩展100字节。

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

int main()
{
    int fd = open("helloworld.txt", O_RDWR);
    if(fd == -1)
    {
        perror(open);
    }
    int ret = lseek(fd, 100, SEEK_END);  //扩展100字节
    if(ret == -1)
    {
        perror(lseek);
        return -1;
    }
    write(fd," ", 1);  //写入一个空格,否则扩展后的文件大小不变
    close(fd);
    return 0;
}

原来文件的大小是11字节

文件内容:

编译并执行,执行后文件大小变成112字节,多了1字节是因为写入了一个空格

文件内容:

最后有一个写入的空格

stat、lstat

int stat(const char* pathname, struct stat* statbuf);
/*
作用:获取一个文件相关的一些信息,指令 man 2 stat查看详情
头文件:#include <sys/types.h>
       #include <sys/stat.h>
       #include <unistd.h>
pathname:操作的文件的路径
statbuf:结构体变量,传出参数,调用函数后,获取到的文件的信息会保存在这个结构体中
返回值:成功:返回0;失败:返回-1并设置errno
*/

int lstat(const char* pathname, struct stat* statbuf);
/*
作用:获取一个软连接文件相关的一些信息,而不是软连接指向的文件
详情指令、头文件、参数等其他信息与stat相同
*/
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;      //最后一次改变时间(指属性)
};
stat结构体中的st_mode变量


说明:st_mode是一个16位的二进制数,位置对应的信息如上图所示,分别用0和1表示。图下方的数字都是八进制数,除了权限是一个位上0和1代表有或没有该权限外,文件类型是以多个二进制位代表不同的文件,例如管道的10000,转换为二进制数是0001 0000 0000 0000,即表示文件类型的四个二进制位从左到右数第一到三位都是0,第四位是1,代表管道文件。又例如套接字的140000,转换为二进制数是1100 0000 0000 0000,即表示文件类型的四个二进制位从左到右数第一和第二位都是1,第三和第四位是0,代表套接字。如果想知道某个文件是否有某个权限,可以将该文件的st_mode与图下方对应权限的宏做 & 操作。如果结果是0则代表没有该权限,否则就是有。如果要判断某个文件的类型,可以将该文件的st_mode与图下方S_IFMT掩码做 & 操作,得到的值再与宏作比较即可得到文件的类型

06 文件属性操作函数

int access(const char* pathname, int mode);
/*
作用:判断某个文件是否有某个权限,或者判断文件是否存在,指令 man 2 access 查看详情
头文件: #include <unistd.h>
pathname:判断的文件路径
mode:
    R_OK:判断是否有读权限
    W_OK:判断是否有写权限
    x_OK:判断是否有执行权限
    F_OK:判断文件是否存在
返回值:成功返回0,失败返回-1
*/


int chmod(const char* pathname, mode_t mode);
/*
修改文件的权限,指令 man 2 chmod 查看详情
头文件:#include <sys/stat.h>
pathname:需要修改的文件的路径
mode:需要修改的权限值,八进制的数。有许多宏选项,指令 man 2 chmod 查看
返回值:成功返回0,失败返回-1
*/


int chown(const char* path, uid_t owner, gid_t group);
/*
修改文件的所有者和所在组。有权限要求。指令 man 2 chown 查看详情
头文件:#include <unistd.h>
path:文件路径
owner:修改后的所有者id
group:修改后的组id
返回值:成功返回0,失败返回-1
*/


int truncate(const char* path, off_t length);
作用:缩减或者扩展文件的尺寸至指定的大小。缩减会删除超过空间的字符,扩展则用空字符填充。指令 man 2 truncate 查看详情
头文件:
    #include <unistd.h> 
    #include <sys/types.h>
path:需要修改的文件的路径
length:需要最终文件变成的大小
返回值:成功返回0,失败返回-1

07 目录操作函数

int mkdir(const char *pathname, mode_t mode);
/*
作用:创建一个目录。指令 man 2 mkdir 查看详情
头文件:
    #include <sys/stat.h>
    #include <sys/types.h>
pathname:创建的目录的路径
mode:权限,八进制的数。如:0777,但是最终结果不一定是0777,因为最终权限的结果是:mode & (~umask) & 0777
返回值:成功返回0,失败返回-1
*/


int rmdir(const char *pathname);
/*
作用:删除空目录,如果目录里有内容则无法删除。指令 man 2 rmdir 查看详情
头文件:#include <unistd.h>
pathname:删除的目录的路径
返回值:成功返回0,失败返回-1
*/


int rename(const char *oldpath, const char *newpath);
/*
作用:重命名一个文件,也可以将文件移动到其他目录。指令 man 2 rename 查看详情
头文件:#include <stdio.h>
oldpath:旧路径
newpath:新路径
返回值:成功返回0,失败返回-1
*/


int chdir(const char *path);
/*
作用:修改进程的工作目录。比如在/home/linux目录下启动了一个可执行程序a.out,进程的工作目录就是/home/linux。指令 man 2 chdir 查看详情
头文件:#include <unistd.h>
path:修改后的工作目录
返回值:成功返回0,失败返回-1
*/


char *getcwd(char *buf, size_t size);
/*
作用:获取当前工作目录。指令 man 2 getcwd 查看详情
头文件:#include <unistd.h> 
buf:存储的路径,指向的是一个数组(传出参数)
size:数组buf的大小
返回值:第一个参数buf的内存地址
*/

08 目录遍历函数

DIR *opendir(const char *name);
/*
打开一个目录。指令 man 3 opendir 查看详情
头文件:#include <sys/types.h>
       #include <dirent.h>
name:要打开的目录名称
返回值:DIR 类型的结构体,可以理解为目录流
       出现错误返回NULL
*/


struct dirent *readdir(DIR *dirp);
/*
读取目录中的内容。指令 man 3 readdir 查看详情
头文件:#include <dirent.h>
dirp:opendir返回的结果
返回值:dirent 类型的结构体(具体内容见下方),代表读取到的文件的信息。开始时在目录开头(不是第一个文件,而是更前面的位置),每读取一次会往后走,第一次读取会移动到第一个文件(当前目录“.”),每次返回当前读取到的文件信息,读取到末尾或读取失败返回NULL,读取到末尾时errno不变,失败时errno会被新设置
*/

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


int closedir(DIR *dirp);
/*
关闭读取的目录,指令 man 3 closedir 查看详情
头文件:#include <sys/types.h>
       #include <dirent.h>
dirp:opendir返回的结果
返回值:成功返回0,失败返回-1
*/

09 文件描述符函数

int dup(int oldfd);
/*
作用:复制一个新的文件描述符。假设fd指向的是a.txt, fd1 = dup(fd),fd1也是指向a. txt,且fd1是从空闲的文件描述符表中找一个最小的,作为新的拷贝的文件描述符。指令 man 3 dup 查看详情。
头文件:#include <unistd.h>
oldfd:要被复制的文件描述符
返回值:复制完成的新的文件描述符
*/

int dup2(int oldfd, int newfd);
作用:重定向文件描述符。oldfd指向a.txt,newfd指向b.txt,调用函数成功后:newfd和b.txt做close,newfd指向了a.txt。oldfd必须是个有效的文件描述符,oldfd和newfd值相同,相当于什么都没有做。指令 man 3 dup2 查看详情。
头文件:#include <unistd.h>
返回值:newfd,失败返回-1

10 fcntl函数

man 2 fcntl

int fcntl(int fd, int cmd, ...);
/*
对文件描述符进行某些操作,最常用的就是复制文件描述符和修改文件描述符的flags。指令 man 2 fcntl 查看详情。
fd:表示需要操作的文件描述符
cmd:表示对文件描述符进行如何操作
    F_DUPFD:复制文件描述符,复制的是第一个参数fd,得到一个新的int fd1 = fcntl(fd, F_DUPFD);
    F_GETFL:获取指定的文件描述符文件状态flags。获取的flag和open函数传递的flags是一个东西。
    F_SETFL:设置文件描述符文件状态flag。
必选项:O_RDONLY,O_WRONLY,O_RDWR,不可以被fcntl修改
可选性:可选性: O_APPEND(表示追加数据),O_NONBLOCK(设置成非阻塞)。阻塞和非阻塞:描述的是函数调用的行为。
返回值:根据cmd的不同,返回值也不同。成功的情况下:F_DUPFD返回复制的新的文件描述符,F_GETFL返回得到的flags,F_SETFL返回0;失败的情况返回-1
*/

//示例:修改文件描述符fd的flag,使其可以追加数据
int fd = open("a.txt", O_RDWR);  //注意这里必须有写的权限,因为fcntl不能修改必选项的三个flag,所以初始打开文件的时候必须有写的权限。
int flag = fcntl(fd, F_GETFL);   //得到原来fd的flag
flag |= O_APPEND;    //用 | 操作追加O_APPEND
fcntl(fd, F_SETFL, flag);   //设置新的flag

posted @ 2022-09-10 21:41  小肉包i  阅读(50)  评论(0)    收藏  举报