【Linux Server】一、Linux系统编程入门

一、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 中的其它规则一般都是为第一条规则服务的。

例子:
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函数为目标文件描述符指定宿主进程或进程组,那么被指定的宿主进程或进程组将捕获这两个信号。
  • 其他

    • 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;
}
上一篇:服务器终端性能测试之stream


下一篇:DevOps落地三部曲:如何归责?用啥工具?往哪里去?