一、Linux系统编程入门
1.安装命令 sudo apt install gcc g++
查看版本 gcc/g++ -v/–version
2.GCC常用参数选项
gcc编译选项 | 说明 |
---|---|
-E | 预处理指定的源文件,不进行编译 |
-S | 编译指定的源文件,但是不进行汇编 |
-c | 编译、汇编指定的源文件,但是不进行链接 |
-o [file1] [file2] / [file2] -o [file1] | 将文件 file2 编译成可执行文件 file1 |
-I directory | 指定 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 |
3.在编译阶段,g++ 会调用 gcc,对于 C++ 代码,g++ 和 gcc 是等价的,但是因为 gcc 命令不能自动和 C++ 程序使用的库链接,所以通常用 g++ 来完成链接。
编译可以用 gcc/g++,而链接可以用 g++ 或者 gcc -lstdc++。
编译可执行文件test01:g++ test01.cpp -o test01
4.静态库在程序的链接阶段被复制到程序中;动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态库加载到内存中供程序调用。
5.静态库的制作:
- gcc 获得.o文件
- 将.o文件打包,使用ar工具(archive)
ar rcs libxxx.a file1.o file2.o file3.0
r - 将文件插入备存文件中
c - 建立备存文件
s - 索引
gcc -c file1.c file2.c file3.c
ar rcs libxxx.a file1.o file2.o file3.o
6.查看文件目录树的命令是tree
sudo apt install tree
7.静态库的使用:
要头文件(放在include目录下)和libcalc.a文件(放在lib目录下)。
gcc main.c -o app -I ./include -l calc -L ./lib
注:calc是库的名字,libcalc.a是库文件的名字。
8.动态库的制作:
- gcc 获得.o文件。用-fpic得到和位置无关的代码,在x86架构-fpic和-fPIC没有区别。
gcc -c -fpic/-fPIC file1.c file2.c file3.c - gcc 得到动态库。
gcc -shared file1.o file2.o file3.o -o libxxx.so
在Linux下,动态库是一个可执行文件。
9.动态库的使用:
gcc main.c -o app -I ./include -L ./lib -l calc
动态库可以实现进程间资源共享。两个进程加载同一个动态库,共享一份内存空间,但共享的是代码段,并不是数据段。
- 静态库:GCC 进行链接时,会把静态库中代码打包到可执行程序中。
- 动态库:GCC 进行链接时,动态库的代码不会被打包到可执行程序中。
- 程序启动之后,动态库会被动态加载到内存中,通过 ldd (list dynamic dependencies)命令检查动态库依赖关系。
ldd main - 如何定位共享库文件呢?
当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径。此时就需要系统的动态载入器来获取该绝对路径。对于elf格式的可执行程序,是由ld-linux.so来完成的,它先后搜索elf文件的
DT_RPATH段 ——> 环境变量LD_LIBRARY_PATH ——> /etc/ld.so.cache文件列表 ——> /lib/,/usr/lib
目录找到库文件后将其载入内存。
方式一:配置环境变量(临时的):
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/bread/testcode/lib
echo $LD_LIBRARY_PATH
方式二:配置环境变量(永久的,用户级别):
vim .bashrc
最后一行加上export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/bread/testcode/lib
保存退出后 source .bashrc
方式三:配置环境变量(系统级别):
sudo vim /etc/profile
最后一行加上export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/bread/testcode/lib
保存退出后 source /etc/profile
方式四:间接配置/etc/ld.so.cache文件列表的方式:
sudo vim /etc/ld.so.conf
最后一行加上/home/bread/testcode/lib
保存退出后 sudo ldconfig
10.Makefile文件命名和规则:
- 文件命名:makefile 或者 Makefile
- Makefile规则:
- 一个 Makefile 文件中可以有一个或者多个规则
目标 … : 依赖 …
命令(Shell 命令)
…
其中,
目标:最终要生成的文件(伪目标除外)
依赖:生成目标所需要的文件或是目标
命令:通过执行命令对依赖操作生成目标(命令前必须 Tab 缩进) - Makefile 中的其它规则一般都是为第一条规则服务的。
- 一个 Makefile 文件中可以有一个或者多个规则
例子:
vim Makefile
app:file1.c file2.c file3.c
gcc file1.c file2.c file3.c -o app
sudo apt install make
make
11.Makefile工作原理:
- 命令在执行之前,需要先检查规则中的依赖是否存在。如果存在,执行命令;如果不存在,向下检查其它的规则,检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该规则中的命令。
- 检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间。如果依赖的时间比目标的时间晚,需要重新生成目标;如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被执行。
app:file1.o file2.o file3.o
gcc file1.o file2.o file3.o -o app
file1.o:file1.c
gcc -c file1.c -o file1.o
file2.o:file2.c
gcc -c file2.c -o file2.o
file3.o:file3.c
gcc -c file3.c -o file3.o
12.Makefile中的变量:
- 自定义变量
变量名=变量值 var=hello - 预定义变量
AR : 归档维护程序的名称,默认值为 ar
CC : C 编译器的名称,默认值为 cc
CXX : C++ 编译器的名称,默认值为 g++
$@ : 目标的完整名称
$< : 第一个依赖文件的名称
$^ : 所有的依赖文件 - 获取变量的值
$(变量名)
模式匹配:
- %.o:%.c
- %:通配符,匹配一个字符串
- 两个%匹配的是同一个字符串
- %.o:%.c
gcc -c $< -o $@
app:file1.c file2.c file3.c
gcc -c file1.c file2.c file3.c
# 自动变量只能在规则的命令中使用
app:file1.c file2.c file3.c
$(CC) -c $^ -o $@
#定义变量
src=file1.o file2.o file3.o
target=app
$(target):$(src)
$(CC) $(src) -o $(target)
%.o:%.c
$(CC) -c $< -o $@
13.Makefile中的函数:
- $(wildcard PATTERN…)
- 功能:获取指定目录下指定类型的文件列表
- 参数:PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,一般使用空格间隔
- 返回:得到的若干个文件的文件列表,文件名之间使用空格间隔
- 示例:
$(wildcard *.c ./sub/ *.c)
返回值格式: a.c b.c c.c d.c e.c f.c
- $(patsubst ,,
) - 功能:查找
中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式,如果匹配的话,则以替换。 - 可以包括通配符
%
,表示任意长度的字串。如果中也包含%
,那么,中的这个%
将是中的那个%所代表的字串。(可以用\
来转义,以\%
来表示真实含义的%
字符) - 返回:函数返回被替换过后的字符串
- 示例:
$(patsubst %.c, %.o, x.c bar.c)
返回值格式: x.o bar.o
- 功能:查找
#定义变量
#获取file1.c file2.c file3.c
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
删除.o文件:make clean
14.GDB调试:
- 通常,在为调试而编译时,我们会关掉编译器的优化选项(
-O
), 并打开调试选项(-g
)。另外,-Wall
在尽量不影响程序行为的情况下选项打开所有warning,也可以发现许多问题,避免一些不必要的BUG。 - gcc -g -Wall program.c -o program
-g
选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时才能保证 gdb 能找到源文件。
15.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 行数
gcc test.c -o test -g
ll -h test // test加了编译选项-g后,编译的可执行文件变大
gdb test
(gdb) set args 10 20
(gdb) show args
(gdb) list // 输入list(或者l)默认显示源文件test.c的前10行
(gdb) list // 输入list(或者l或者直接回车)继续往下显示源文件test.c
(gdb) help
(gdb) help all
(gdb) help set
(gdb) quit
16.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
g++ main.cpp -o main -g
gdb main
(gdb) break 9 // 在第9行打断点,但是第9行还没有执行的
(gdb) info break // 查看断点信息
(gdb) break main // 在main函数所在行打断点
(gdb) delete 2 // 删除编号为2的断点
(gdb) break 10 if i==5 // 在第10行当i==5的时候断点
17.GDB 命令 – 调试命令
- 运行GDB程序
- start(程序停在第一行)
- run(遇到断点才停)
- 继续运行,到下一个断点停
- c/continue
- 向下执行一行代码(不会进入函数体)
- n/next
- 变量操作
- p/print 变量名(打印变量值)
- ptype 变量名(打印变量类型)
- 向下单步调试(遇到函数进入函数体)
- s/step
- finish(跳出函数体)
- 自动变量操作
- display 变量名(自动打印指定变量的值)
- i/info display
- undisplay 编号
- 其它操作
- set var 变量名=变量值 (循环中用的较多)
- until (跳出循环)
g++ main.cpp -o main -g
gdb main
(gdb) start // 相当于默认在第一行打了断点,start之后直接停在了第一行
(gdb) c // 第一行后面都没有设置断点,因此c之后直接运行完了程序
(gdb) break 9 // 在第9行打断点
(gdb) run // 停在第9行
(gdb) print i // 打印第9行变量i的值
(gdb) ptype i // 打印第9行变量i的类型
(gdb) set var i=1 // 设置变量i的值等于1
18.文件IO
Linux系统IO函数是没有缓存区的,不同于标准C库的IO函数。
在终端输入 man 2 open 查看Linux库的帮助文档。
在终端输入 man 3 fopen 查看标准C库的帮助文档。
-
Linux 系统 IO 函数
-
int open(const char *pathname, int flags);
打开一个已经存在的文件.
flags是打开文件后对文件的操作权限. -
int open(const char *pathname, int flags, mode_t mode);
创建一个新的文件.
mode是文件的权限属性,但最终权限是 mode&~umask .
可以在终端直接输入umask看当前用户的umask的值。
umask的作用就是抹去某些权限。
int fd = open(“a.txt”, O_RDWR | O_CREAT, 0777);
普通用户的话,得到a.txt的权限一般是0775,因为umask一般是0002。 -
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);
对文件指针进行操作。
offset:偏移量
whence:SEEK_SET-设置文件指针的偏移量、SEEK_CUR-当前位置+offset的偏移量、SEEK_END-文件大小+offset的偏移量。
作用:
移动文件指针到文件头部 lseek(fd, 0, SEEK_SET);
获取当前文件指针的位置 off_t curIndex = lseek(fd, 0, SEEK_CUR);
获取文件长度 off_t len = lseek(fd, 0, SEEK_END);
拓展文件的长度 lseek(fd, nLen, SEEK_END); 最后还要写入数据,文件的长度才会变长。 -
int stat(const char *pathname, struct stat *statbuf);
获取文件的一些属性信息。
也可以直接在终端输入 stat a.txt 查看文件的信息。 -
int lstat(const char *pathname, struct stat *statbuf);
获取软链接文件的一些属性信息。
直接在终端输入创建软连接 ln -s a.txt b.txt
如果用stat b.txt获取到的是a.txt的文件信息。
-
-
文件权限
- 第15位-第12位:4位-表示文件类型
- 第11位-第9位:3位-表示特殊权限位
- 第8位-第6位:3位-分别表示rwx,当前用户.
- 第5位-第3位:3位-分别表示rwx,当前用户所在的组.
- 第2位-第0位:3位-分别表示rwx,其他组.
-
文件属性操作函数
-
int access(const char *pathname, int mode);
作用:判断某个文件是否有某个权限或者判断文件是否存在。
mode:R_OK判断是否有读权限、W_OK判断是否有写权限、X_OK判断是否有执行权限、F_OK判断文件是否存在。 -
int chmod(const char *filename, int mode);
作用:修改文件的权限。 -
int chown(const char *path, uid_t owner, gid_t group);
作用:修改文件的所在组或所有者。
查看用户id:vim /etc/passwd
查看组id:vim /etc/group
或者查看id用:id xiaoming -
int truncate(const char *path, off_t length);
作用:对文件尺寸缩减或者扩展至指定大小。
length:需要最终文件变成的大小。
-
-
目录操作函数
-
一般来说,Web服务器的逻辑根目录并非文件系统的根目录“/”,而是站点的根目录(对于Linux的Web服务来说,该目录一般是/var/www/).
-
int rename(const char *oldpath, const char *newpath);
-
int chdir(const char *path);
作用:修改进程的工作目录。 -
char *getcwd(char *buf, size_t size);
作用:获取进程当前的工作目录。
返回值:返回的其实就是第一个参数。 -
int mkdir(const char *pathname, mode_t mode);
作用:创建一个目录。 -
int rmdir(const char *pathname);
作用:删除一个空目录。 -
int chroot(const char* path);
path参数指定要切换到的目标根目录。该函数并不改变进程的当前工作目录。此外,只有特权进程才能改变根目录。
-
-
目录遍历函数
-
DIR *opendir(const char *name);
作用:打开一个目录,返回目录流。 -
struct dirent *readdir(DIR *dirp);
作用:读取目录中的数据,返回读取到的文件的信息。如果读取到了末尾或者失败了,返回NULL。 -
int closedir(DIR *dirp);
-
-
dup和dup2函数
-
int dup(int oldfd);
复制得到一个新的文件描述符(从空闲的文件描述符表中找一个最小的),与oldfd指向同一个文件。 -
int dup2(int oldfd, int newfd);
重定向文件描述符。调用函数成功后,newfd close掉原来它所指向的文件,然后指向和oldfd所指向的文件。
返回值和newfd的值相同。 -
注意:通过dup和dup2创建的文件描述符并不继承原文件描述符的属性,比如close-on-exec和non-blocking等。
-
-
readv函数和writev函数
- readv函数将数据从文件描述符读到分散的内存块中,即分散读;writev函数则将多块分散的内存数据一并写入文件描述符中,即集中写。
- #include <sys/uio.h>
- ssize_t readv(int fd, const struct iovec* vector, int count);
- ssize_t writev(int fd, const struct iovec* vector, int count);
-
sendfile函数
- sendfile函数在两个文件描述符之间直接传递数据(完全在内核中操作)从而避免了内核缓冲区和用户缓冲区之间数据拷贝,效率很高,这被称为零拷贝。
- #include <sys/sendfile.h>
- ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
out_fd是待写入内容的文件描述符;
in_fd是待读出内容的文件描述符;
offset指定从读入文件流的哪个位置开始读,如果为空,则使用读入文件流默认的起始位置。
count指定在文件描述符in_fd和out_fd之间传输的字节数。
in_fd必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket和管道;而out_fd则必须是一个socket。
-
splice函数
- splice函数用于在两个文件描述符之间移动数据,也是零拷贝操作。
- #include <fcntl.h>
- ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags);
fd_in是待输入的文件描述符。如果fd_in是一个管道文件描述符,那么off_in参数必须被设置为NULL。 - 使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符。
-
tee函数
- tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作。它不消耗数据(和splice函数不同,它是复制),因此源文件描述符上的数据仍然可以用于后续的读操作。
- #include <fcntl.h>
- ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
- fd_in和fd_out必须都是管道文件描述符。
-
fcntl 函数
- int fcntl(int fd, int cmd, … /* arg */ );
作用是:复制文件描述符;设置/获取文件的状态标志等等。
cmd表示对文件描述符进行如何操作。
F_DUPFD是复制文件描述符,新文件描述符通过返回值返回
F_GETFL是获取指定文件描述符的文件状态,也就是open函数设置的那个flag。
F_SETFL是设置指定文件描述符的文件状态,但不能设置文件权限属性。常用的状态标志如:O_APPEND表示追加数据。O_NONBLOCK表示设置为不阻塞。 - 在网络编程中,fcntl函数通常用来将一个文件描述符设置为非阻塞的。
- SIGIO和SIGURG这两个信号与其他Linux信号不同,它们必须与某个文件描述符相关联方可使用:当被关联的文件描述符可读或可写时,系统将触发SIGIO信号;当被关联的文件描述符(而且必须是一个socket)上有带外数据可读时,系统将触发SIGURG信号。将信号和文件描述符关联的方法,就是使用fcntl函数为目标文件描述符指定宿主进程或进程组,那么被指定的宿主进程或进程组将捕获这两个信号。
- int fcntl(int fd, int cmd, … /* arg */ );
-
其他
- errno:属于Linux系统函数库,库里面的一个全局变量,记录的是最近的错误号。
- perror:属于标准C库,打印errno对应的错误描述。
int fd = open("a.txt", O_RDONLY); if(-1 == fd) perror("Error"); //如果没有这个文件,会输出Error:No such file or directory
- 传递给main函数的第一个参数argv[0]是可执行程序的名称。
例如 C:\Desktop\build-untitled-Desktop_Qt_5_7_0_MinGW_32bit-Debug\debug\untitled.exe - 阻塞和非阻塞:描述的是函数调用的行为。
19.模拟实现ls -l a.txt命令
#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>
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;
}