0%

C++ 开发项目学习笔记

前言

C++ 开发项目的学习笔记,来自牛客,源视频地址:https://www.nowcoder.com/study/live/504.

Linux系统编程入门

Linux 开发环境搭建

很基础的内容,以前已经配过啦,就不赘述了,目前我使用的环境是:

  • 本地操作环境:Window 10
  • Linux: Ubuntu 18.04 (实验室的 Linux 服务器)
  • IDE:VSCode,本地编程,使用 SFTP 插件同步到服务器
  • SSH 远程:MobaXterm
  • SFTP 文件传输:FileZilla

GCC

简介

  • 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 gcc g++ (版本> 4.8.5)
  • 查看版本:gcc/g++ -v/--version

工作流程

image-20230514132310529

image-20230514132326939

  • 预处理包括:将头文件添加的目标文件,删掉注释,宏替换等,命令为 gcc hello.c -E -o hello.i
  • 编译器将预处理后的代码编译为汇编代码,命令为 gcc hello.i -S -o hello.s
  • 汇编代码将汇编代码汇编为机器可以执行的二进制文件,命令为 gcc hello.s -c -o hello.o

gcc 与 g++ 的区别

  • gcc 和g++都是GNU(组织)的一个编译器。
  • 误区一:gcc 只能编译c 代码,g++ 只能编译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,二者等价

参数表

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

静态库

简介

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

命名规则

  • Linux: lib(前缀)+库名字+.a(后缀)
  • windows: libxxx.lib

制作方法

  1. gcc 获得 .o 文件
  2. 使用 ar 工具 (archive) 打包:ar rcs libxxx.a xxx1.o xxx2.o
    • r –将文件插入备存文件中
    • c –建立备存文件
    • s –索引

静态库的使用示例:

文件树:

1
2
3
4
5
6
7
8
9
10
├── include
│   └── head.h
├── lib
│   └── libcalc.a
├── main.c
└── src
├── add.c
├── div.c
├── mult.c
└── sub.c
  1. 使用 gcc -c add.c sub.c mult.c div.c -I ../include/ 获得 .o 文件
  2. 使用 ar rcs libcalc.a add.o div.o mult.o sub.o 获得 libcalc.a 库文件,移动到 lib 目录下
  3. 编译 main.cgcc main.c -o main.a -I include/ -l calc -L ./lib/

动态库

命名规则

  • Linux: lib(前缀)+库名字+.so(后缀)
  • windows: libxxx.dll

制作方法

  1. gcc 获得 .o 文件,得到与位置无关的代码:gcc -c -fpic a.c b.c 即添加 -fpic 参数
  2. 获得动态库:gcc -shared a.o b.o -o libcalc.so

动态库的使用示例:

文件树:

1
2
3
4
5
6
7
8
9
10
├── include
│   └── head.h
├── lib
│   └── libcalc.so
├── main.c
└── src
├── add.c
├── div.c
├── mult.c
└── sub.c
  1. 使用 gcc -c -fpic add.c sub.c mult.c div.c -I ../include/ 获得 .o 文件
  2. 使用 gcc -shared *.o -o libcalc.so 获得 libcalc.so 库文件,移动到 lib 目录下
  3. 编译 main.cgcc main.c -o main.aa -I include/ -l calc -L ./lib/

但此时如果直接 运行会报错:./main.aa: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory 即无法找到动态库

动态库的原理

  • 静态库:GCC 进行链接时,会把静态库中代码打包到可执行程序中
  • 动态库:GCC 进行链接时,动态库的代码不会被打包到可执行程序中
  • 程序启动之后,动态库会被动态加载到内存中,通过ldd(list dynamic dependencies)命令检查动态库依赖关系
  • 如何定位共享库文件呢?当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径。此时就需要系统的动态载入器来获取该绝对路径。对于elf格式的可执行程序,是由ld-linux.so来完成的,它先后搜索elf文件的DT_RPATH段——> 环境变量LD_LIBRARY_PATH ——> /etc/ld.so.cache文件列表——> /lib/,/usr/lib目录找到库文件后将其载入内存。

使用 ldd 分析可执行文件 main.aa 得到

1
2
3
4
linux-vdso.so.1 (0x00007ffd3cd51000)
libcalc.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2ca5e95000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2ca6488000)

解决方法是

  • 将动态库路径添加到环境变量
    • 只是暂时性添加到环境变量:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/user/webserver/library/lib, 最后的是动态库的绝对路径,该方法关闭 SSH 后就失效了
    • 用户级别,将上述代码添加到 .bashrc,重启或刷新(source ~/.bashrc)
    • 系统级别,将上述代码添加到 /etc/profile 中,重启或刷新
    • 可以使用 env 查看
  • 添加到 /etc/ld.so.cache 文件列表
    • /etc/ld.so.cache 是一个二进制文件不能直接修改,需要将绝对路径添加到 /etc/ld.so.conf 中,再刷新 ldconfig 即可
  • 放到/lib/,/usr/lib目录
    • 但不推荐这种方法,因为这两个目标本身有很多库文件,不推荐将自己的库文件放到一起

MakeFile

MakeFile简介

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

文件命名:必须为 makefile 或 Makefile

Makefile 基本规则

1
2
3
4
# 注释用 #标记
目标 ...: 依赖 ...
命令
...

例如:

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

执行 make 即可运行 Makefile

  • 一个Makefile 文件中可以有一个或者多个规则
    • 目标:最终要生成的文件(伪目标除外)
    • 依赖:生成目标所需要的文件或是目标
    • 命令:通过执行命令对依赖操作生成目标(命令前必须Tab 缩进)
  • Makefile 中的其它规则一般都是为第一条规则服务的(与第一条规则无关的不会执行)
  • 命令在执行之前,需要先检查规则中的依赖是否存在
    • 如果存在,执行命令
    • 如果不存在,向下检查其它的规则,检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该规则中的命令
  • 检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间
    • 如果依赖的时间比目标的时间晚,需要重新生成目标
    • 如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被执行

关于依赖和更新的示例:

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

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 没有数据结构,所有变量都可以视为字符串
    • 语法:变量名=变量值var=hello, 引用变量 $(var)
  • 一些预定义变量
    • AR : 归档维护程序的名称,默认值为ar
    • CC: C 编译器的名称,默认值为cc
    • CXX : C++ 编译器的名称,默认值为g++
  • 自动变量(只能再规则命令中使用)
    • $@ : 目标的完整名称
    • $< : 第一个依赖文件的名称
    • $^: 所有的依赖文件
  • 模式匹配
    • %.o:%.c
    • %: 通配符,匹配一个字符串
    • 两个%匹配的是同一个字符串
  • 函数
    • $(wildcard PATTERN...)
    • 功能:获取指定目录下指定类型的文件列表
    • 参数:PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,一般使用空格间隔
    • 返回:得到的若干个文件的文件列表,文件名之间使用空格间隔
    • 示例:$(wildcard *.c ./sub/*.c)
    • $(patsubst <pattern>,<replacement>,<text>)
    • 功能:查找 <text> 中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式 <pattern>,如果匹配的话,则以 <replacement> 替换。
    • <pattern>可以包括通配符%,表示任意长度的字串。如果<replacement>中也包含%,那么,<replacement>中的这个%将是<pattern>中的那个%所代表的字串。(可以用\来转义,以\%来表示真实含义的%字符)
    • 返回:函数返回被替换过后的字符串
    • 示例:$(patsubst %.c, %.o, x.c bar.c)

改写过的示例

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

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

# 伪目标,不会生成一个真的文件
.PHONE:clean
clean:
rm -f $(objs)

正常执行 make 并不会执行 clean,需要指定:make clean

GDB调试

GDB简介

  • GDB 是由GNU 软件系统社区提供的调试工具,同GCC 配套组成了一套完整的开发环境,GDB 是Linux 和许多类Unix 系统中的标准开发环境。
  • 一般来说,GDB 主要帮助你完成下面四个方面的功能:
    • 启动程序,可以按照自定义的要求随心所欲的运行程序
    • 可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式)
    • 当程序被停住时,可以检查此时程序中所发生的事
    • 可以改变程序,将一个BUG 产生的影响修正从而测试其他BUG
  • 通常,在为调试而编译时,我们会()关掉编译器的优化选项(-O),并打开调试选项(-g)。另外,-Wall在尽量不影响程序行为的情况下选项打开所有warning,也可以发现许多问题,避免一些不必要的BUG。
  • gcc -g -Wall program.c -o program
  • -g 选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证gdb 能找到源文件

GDB命令

基本命令

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

断点操作

命令 代码
设置断点 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

调试命令

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

文件IO

image-20230526145334956

标准C库与Linux的 IO 函数的区别

  1. 标准C库的函数是跨平台的,是通过调用不同平台的系统API实现的
  2. 标准C库的IO函数有缓冲区,效率更高一些

image-20230526152337091

虚拟地址空间

image-20230526153550989

通过 MMU 将虚拟地址空间映射到物理地址空间

文件描述符

文件描述符保存在Linux内核区

image-20230526153950771

PCB 中有个文件描述符表(数组),前三个固定是标准输入输出标准错误,对应当前终端。

文件可以重复打开,文件描述符不一样

Linux系统IO函数

Linux的系统API函数可以通过 man 2 函数名 查看说明文档,man 3 函数名 查看标准 C 库函数说明文档

open 函数:打开文件

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

// 打开一个已经存在的文件
int open(const char *pathname, int flags);
参数:
- pathname:要打开的文件路径
- flags:对文件的操作权限设置还有其他的设置
O_RDONLY, O_WRONLY, O_RDWR 这三个设置是互斥的
返回值:返回一个新的文件描述符,如果调用失败,返回-1

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

#include <stdio.h>
void perror(const char *s);作用:打印errno对应的错误描述
s参数:用户描述,比如hello,最终输出的内容是 hello:xxx(实际的错误描述)


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

open 函数:创建文件,与打开文件不同的是添加“不存在则创建”的 flags.

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 <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_CREAT 文件不存在,创建新文件
- mode:八进制的数,表示创建出的新的文件的操作权限,比如:0775
最终的权限是:mode & ~umask
0777 -> 111111111
& 0775 -> 111111101
----------------------------
111111101
按位与:0和任何数都为0
umask的作用就是抹去某些权限。

flags参数是一个int类型的数据,占4个字节,32位。
flags 32个位,每一位就是一个标志位。
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {

// 创建一个新的文件
int fd = open("create.txt", O_RDWR | O_CREAT, 0777);

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

// 关闭
close(fd);

return 0;
}

umask 的值可以直接在linux终端输入 umask 查看

读写函数: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
/*  
#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
*/

lseek 函数,用于移动文件指针

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
/*  
标准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操作某个文件
- 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)
注意:需要写一次数据

*/

stat 函数:获取文件属性,与终端使用 stat 效果类似

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
/*
#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>
#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;
}

stat 结构体定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat {
dev_tst_dev; // 文件的设备编号
ino_tst_ino; // 节点
mode_tst_mode; // 文件的类型和存取的权限
nlink_tst_nlink; // 连到该文件的硬连接数目
uid_tst_uid; // 用户ID
gid_t st_gid; // 组ID
dev_tst_rdev; // 设备文件的设备编号
off_tst_size; // 文件字节数(文件大小)
blksize_tst_blksize;// 块大小
blkcnt_tst_blocks; // 块数
time_tst_atime; // 最后一次访问时间
time_tst_mtime; // 最后一次修改时间
time_tst_ctime; // 最后一次改变时间(指属性)
};

st_mode 变量

image-20230526160716438

模拟实现 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

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <string.h>

// 模拟实现 ls -l 指令
// -rw-rw-r-- 1 nowcoder nowcoder 12 12月 3 15:48 a.txt
int main(int argc, char * argv[]) {

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

// 通过stat函数获取用户传入的文件的信息
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] = 's';
break;
case S_IFIFO:
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 * fileGrp = getgrgid(st.st_gid)->gr_name;

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

// 获取修改的时间
char * time = ctime(&st.st_mtime);

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, fileGrp, fileSize, mtime, argv[1]);

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

return 0;
}

文件属性操作函数

access

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

chmod

1
2
3
4
5
6
7
8
9
10
/*
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
修改文件的权限
参数:
- pathname: 需要修改的文件的路径
- mode:需要修改的权限值,八进制的数
返回值:成功返回0,失败返回-1

*/

chown: 修改文件所有者和所在组

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

使用 man 2 chown 可以查看帮助文档,所有者 ID 与所在组 ID 可以查看 /etc/passwd 文件或 /etc/group,亦可以在终端 id 用户名 查看

truncate

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

目录操作函数

都是常用操作了,作用显而易见

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
#include <unistd.h>
int chdir(const char *path);
作用:修改进程的工作目录
比如在/home/nowcoder 启动了一个可执行程序a.out, 进程的工作目录 /home/nowcoder
参数:
path : 需要修改的工作目录

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

*/

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

/*
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
*/

/*
int rmdir(const char *pathname);
只能删除空目录
*/

目录遍历函数

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
/*
// 打开一个目录
#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,代表读取到的文件的信息
读取到了末尾或者失败了,返回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);
}

struct dirent *ptr;

// 记录普通文件的个数
int total = 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);
total += getFileNum(newpath);
}

if(ptr->d_type == DT_REG) {
// 普通文件
total++;
}


}

// 关闭目录
closedir(dir);

return total;
}

dirent 结构体和 d_type(宏值)

image-20230526162446992

文件描述符操作函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
#include <unistd.h>
int dup(int oldfd);
作用:复制一个新的文件描述符
fd=3, int fd1 = dup(fd),
fd指向的是a.txt, fd1也是指向a.txt
从空闲的文件描述符表中找一个最小的,作为新的拷贝的文件描述符
*/

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

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

int fcntl(int fd, int cmd, ...);
有很多作用,看 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 表示追加数据
NONBLOK 设置成非阻塞

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

Linux多进程开发

进程概述

程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程:

  • 二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息。内核利用此信息来解释文件中的其他信息。(ELF可执行连接格式)
  • 机器语言指令:对程序算法进行编码。
  • 程序入口地址:标识程序开始执行时的起始指令位置。
  • 数据:程序文件包含的变量初始值和程序使用的字面量值(比如字符串)。
  • 符号表及重定位表:描述程序中函数和变量的位置及名称。这些表格有多重用途,其中包括调试和运行时的符号解析(动态链接)。
  • 共享库和动态链接信息:程序文件所包含的一些字段,列出了程序运行时需要使用的共享库,以及加载共享库的动态连接器的路径名。
  • 其他信息:程序文件还包含许多其他信息,用以描述如何创建进程。
  • 进程是正在运行的程序的实例。是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
  • 可以用一个程序来创建多个进程,进程是由内核定义的抽象实体,并为该实体分配用以执行程序的各项系统资源。从内核的角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。记录在内核数据结构中的信息包括许多与进程相关的标识号(IDs)、虚拟内存表、打开文件的描述符表、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录和大量的其他信息。

单道与多道程序

  • 单道程序,即在计算机内存中只允许一个的程序运行。
  • 多道程序设计技术是在计算机内存中同时存放几道相互独立的程序,使它们在管理程序控制下,相互穿插运行,两个或两个以上程序在计算机系统中同处于开始到结束之间的状态, 这些程序共享计算机系统资源。引入多道程序设计技术的根本目的是为了提高 CPU 的利用率。
  • 对于一个单 CPU 系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽然都已经开始运行,但就微观而言,任意时刻,CPU 上运行的程序只有一个。
  • 在多道程序设计模型中,多个进程轮流使用 CPU。而当下常见 CPU 为纳秒级,1秒可以执行大约 10 亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。

时间片

  • 时间片(timeslice)又称为“量子(quantum)”或“处理器片(processor slice)”是操作系统分配给每个正在运行的进程微观上的一段 CPU 时间。事实上,虽然一台计算机通常可能有多个 CPU,但是同一个 CPU 永远不可能真正地同时运行多个任务。在只考虑一个 CPU 的情况下,这些进程“看起来像”同时运行的,实则是轮番穿插地运行,由于时间片通常很短(在 Linux 上为 5ms-800ms),用户不会感觉到。
  • 时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。

并发与并行

  • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。
  • 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

进程控制块(PCB)

  • 为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个 PCB(Processing Control Block)进程控制块,维护进程相关的信息,Linux 内核的进程控制块是 task_struct 结构体。
  • /usr/src/linux-headers-xxx/include/linux/sched.h 文件中可以查看 struct task_struct 结构体定义。其内部成员有很多,我们只需要掌握以下部分即可:
    • 进程id:系统中每个进程有唯一的 id,用 pid_t 类型表示,其实就是一个非负整数
    • 进程的状态:有就绪、运行、挂起、停止等状态
    • 进程切换时需要保存和恢复的一些CPU寄存器
    • 描述虚拟地址空间的信息
    • 描述控制终端的信息
    • 当前工作目录(Current Working Directory)
    • umask 掩码
    • 文件描述符表,包含很多指向 file 结构体的指针
    • 和信号相关的信息
    • 用户 id 和组 id
    • 会话(Session)和进程组
    • 进程可以使用的资源上限(Resource Limit)
      • 可以在终端中使用 ulimit -a 查看可使用资源上限

进程状态转换

进程状态

进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。在五态模型中,进程分为新建态、就绪态,运行态,阻塞态,终止态。

image-20230529195046371

  • 运行态:进程占有处理器正在运行
  • 就绪态:进程具备运行条件,等待系统分配处理器以便运行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列
  • 阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成
  • 新建态:进程刚被创建时的状态,尚未进入就绪队列
  • 终止态:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。进入终止态的进程以后不再执行,但依然保留在操作系统中等待善后。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。

image-20230529195310530

查看进程

命令:ps auxps ajx

  • a:显示终端上的所有进程,包括其他用户的进程
  • u:显示进程的详细信息
  • x:显示没有控制终端的进程
  • j:列出与作业控制相关的信息

进程表中 STAT 参数的含义

SRAT 意义
D 不可中断Uninterruptible(usually IO)
R 正在运行,或在队列中的进程
S(大写) 处于休眠状态
T 停止或被追踪
Z 僵尸进程
W 进入内存交换(从内核2.6开始无效)
X 死掉的进程
< 高优先级
N 低优先级
s 包含子进程
+ 位于前台的进程组

更高级一些的显示进程动态:top

  • 可以在使用top 命令时加上-d来指定显示信息更新的时间间隔,在top 命令执行后,可以按以下按键对显示的结果进行排序:
    • M 根据内存使用量排序
    • P 根据CPU 占有率排序
    • T 根据进程运行时间长短排序
    • U 根据用户名来筛选进程
    • K 输入指定的PID 杀死进程

相关函数

  • 杀死进程
    • kill [-signal] pid
    • kill –l 列出所有信号
    • kill –SIGKILL 进程ID
    • kill -9 进程ID
    • killall name 根据进程名杀死进程
  • 每个进程都由进程号来标识,其类型为pid_t(整型),进程号的范围:0~32767。进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。
  • 任何进程(除init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。
  • 进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当做当前的进程组号。
  • 进程号和进程组相关函数
    • pid_t getpid(void);
    • pid_t getppid(void);
    • pid_t getpgid(pid_t pid);
--- ♥ end ♥ ---

欢迎关注我呀~