Linux高性能服务器开发-第一章

417 阅读4分钟

GCC

image.png gcc -E a.c -o a.i 预处理
gcc -S a.c -o a.s 编译
gcc -c a.c -o a.o 汇编
gcc a.c -o a.out

gcc 和 g++ 区别

  1. 后缀为 .c 的,gcc 把它当作是 C 程序,而 g++ 当作是 c++ 程序
  2. 后缀为 .cpp 的,两者都会认为是 C++ 程序
  3. 编译阶段(链接之前统称编译阶段),g++ 会调用 gcc,对于 C++ 代码,两者是等价的,但是因为 gcc 命令不能自动和 C++ 程序使用的库联接,所以通常用 g++ 来完成链接 gcc test.cpp -c 会生成.o文件.
    gcc test.cpp 会报错,无法自动连接库 stdc++
    gcc -lstdc++ test.cpp 正确

gcc编译选项

image.png image.png -D选项 g++ a.cpp -D DEB

int main(){
#ifdef DEB
    printf("deb\n");
#endif
cout<< "hello"<<endl;
return 0;
}

静态库的制作

静态库在程序的链接阶段被复制 到了程序中;动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加 载到内存中供程序调用。
库的好处:1.代码保密 2.方便部署和分发

命名规则:

Linux : libxxx.a
Windows : libxxx.lib

静态库的制作:

  1. gcc 获得 .o 文件
  2. 将 .o 文件打包,使用 ar 工具(archive) ar rcs libxxx.a xxx.o xxx.o

使用

gcc main.c -I ./include/ -l xxx -L ./lib
-I 指定头文件目录
-L 指定库文件目录
-l 指定链接的静态库, 名字不带前后缀

优缺点

优点:

  • 静态库被打包到应用程序中加载速度快
  • 发布程序无需提供静态库,移植方便
    缺点:
  • 消耗系统资源,浪费内存
  • 更新、部署、发布麻烦

动态库的制作

命名规则

Linux : libxxx.so
Windows : libxxx.dll

制作

  1. gcc -c –fpic/-fPIC a.c b.c
  2. gcc -shared a.o b.o -o libcalc.so -fpic 生成的代码没有绝对地址只有相对地址
    如果有同名的,有比如libxxx.alibxxx.so同时存在,链接.so
    链接之后.so放哪里无所谓,让ld程序找到就行(配置环境变量)

查看程序依赖的动态库

root@aliyun:# ldd a.out

配置环境变量

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

DT_RPATH段 虚拟地址空间的,操作不了

LD_LIBRARY_PATH
修改配置文件,追加export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:real_path
用户级别 root@aliyun:# vim ~/.bashrc文件
系统级别 root@aliyun:# vim /etc/profile 文件
.bashrc 实际是一个shell程序, 登录时执行, 会执行我们写的export

/etc/ld.so.cache文件列表
root@aliyun:# vim /etc/ld.so.conf 直接追加real_path
root@aliyun:# ldconfig 更新

/lib/,/usr/lib
在这两个目录放入.so; 不建议,容易和系统的冲突

优缺点

优点:
可以实现进程间资源共享(共享库)
更新、部署、发布简单
可以控制何时加载动态库
缺点:
加载速度比静态库慢
发布程序时需要提供依赖的动态库

IO

标准C库的函数是可以跨平台的,根据不同平台去实现,内部调用不同系统的api
所有标准C库的IO函数效率高于linux系统api,因为带有缓冲区

文件描述符具体原理

关于文件描述符的具体原理

struct task_struct{
    ...
    //指向所谓的文件描述符表
    struct files_struct* files;
    ...
}

struct files_struct{
    atomic_t count; //引用计数
    struct fdtable* fdt;
    struct fdtable fdtab;
    spinlock_t file_lock ____cacheline_aligned_in_smp;
    int next_fd;
    struct embedded_fd_set close_on_exec_init;
    struct embedded_fd_set open_fds_init;
    
    //所谓的文件描述符表, 文件描述符实际就是数组索引
    struct file* fd_array[NR_OPEN_DEFAULT]; 
}

//文件表结构体, 对应一个iNode节点, 一个iNode节点对应磁盘上的一个文件
struct file{
    mode_t f_mode; //操作文件的权限(是否可读或可写)
    dev_t f_rdev; //用于/dev/tty
    off_t f_ops;//当前文件位移
    unsigned short f_flags;//文件标志: O_RDONLY,O_NONBLOCK,0_SYNC
    unsigned short f_count;//打开的文件数目
    unsigned short f_readda;
    struct inode* f_inode; //指向iNode的结构指针
    struct file_operations* f_op; //文件操作索引指针
};

image.png

image.png

虚拟地址空间

image.png

fopen

fopen 返回的是一个指向FILE结构体的指针 内核会维护进程的文件描述符表, fd对应一个文件描述结构体

image.png

Linux io函数

io

int open(const char *pathname, int flags); 
int open(const char *pathname, int flags, mode_t mode);
- flags:对文件的操作权限和其他的设置
    - 必选项:O_RDONLY, O_WRONLY, O_RDWR 这三个之间是互斥的
    - 可选项:O_CREAT 若文件不存在,创建新文件
    - 按位或
- mode:八进制的数,表示创建出的新的文件的操作权限,比如:0775
    - 最终的权限是:mode & ~umask 结果就是相减,比如0777 & 0002 = 0775
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 设置偏移量:开头位置 + 第二个参数offset的值
    - 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.拓展文件的长度 lseek(fd, 100, SEEK_END) 
        - 注意:需要写一次数据(不然不会扩展)
        
int stat(const char *pathname, struct stat *statbuf); 
int lstat(const char *pathname, struct stat *statbuf);
stat和lstat的区别:当文件是一个符号链接时,lstat返回的是该符号链接本身的信息;

而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_tst_ctime; // 最后一次改变时间(指属性)
};
stat 结构体中的 st_mode 变量 (文件的类型和存取的权限)

image.png

文件属性操作函数

int access(const char *pathname, int mode); 
- 作用:判断某个文件是否有某个权限,或者判断文件是否存在
- 参数:
    - pathname: 判断的文件路径
    - mode:
        - R_OK: 判断是否有读权限
        - W_OK: 判断是否有写权限
        - X_OK: 判断是否有执行权限
        - F_OK: 判断文件是否存在
- 返回值: 成功返回0, 失败返回-1
int chmod(const char *filename, int mode); 
int chown(const char *path, uid_t owner, gid_t group); 
int truncate(const char *path, off_t length);

目录操作函数

int rename(const char *oldpath, const char *newpath); 
int chdir(const char *path); 

char *getcwd(char *buf, size_t size); 
- buf : 存储的路径,指向的是一个数组(传出参数)
- size: 数组的大小
- 返回值:buf

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

目录遍历函数

目录流理解为一个流, 有读写指针(实际就是offset)
每readdir()一次读写指针后移
每次readdir()的单位是一个文件信息(struct dirent)

DIR *opendir(const char *name); 
- name: 需要打开的目录的名称
- 返回值:
    - DIR *类型,理解为目录流
    - 错误返回NULL

struct dirent *readdir(DIR *dirp); 
- 参数:dirp是opendir返回的结果, 目录流
- 返回值:
    - struct dirent *,代表读取到的文件的信息
    - 读取到了末尾或者失败了,返回NULL
- 执行成功向下走一个
    
int closedir(DIR *dirp);
long telldir(DIR *dirp);//返回当前目录流读写指针的位置
void seekdir(DIR *dirp, long loc);//修改当前目录流读写指针的位置
void rewinddir(DIR *dirp); //将目录流读写指针归零
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 - 未知
*/


// 读取某个目录下所有的普通文件的个数
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) {
    DIR * dir = opendir(path);// 1.打开目录流
    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, ".") || !strcmp(dname, "..")) {
            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;
}

dup、dup2、fcntl 函数

int dup(int oldfd); //复制文件描述符
- 作用:复制一个新的文件描述符
int fd1 = dup(fd),
通过dup的fd1 和 fd 指向同一个文件描述结构体,共享读写指针

int dup2(int oldfd, int newfd); //重定向文件描述符
- 调用函数成功后:newfd 指向 oldfd 所指的文件描述结构体
- newfd的值是不变的
- 返回值就是newfd,失败-1
- oldfd 必须是一个有效的文件描述符

int fcntl(int fd, int cmd, ... /* arg */ ); 
- 作用: 复制文件描述符 设置/获取文件的状态标志
- 参数:
    - 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 表示追加数据
                - O_NONBLOK 设置成非阻塞

Makefile

文件命名 makefile 或者 Makefile

Makefile规则

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

目标 ...: 依赖 ...  
    命令(Shell 命令) ...  

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

Makefile 中的其它规则一般都是为第一条规则服务的。默认只会执行第一条规则,如果需要执行后边无关的规则,需要我们指定(make + 规则的目标)可以指定执行某条规则 比如
$: make clean

更新规则

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

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

自定义变量

变量名=变量值 var=hello
Makefile中没有数据类型,都是字符串的形式
查看变量的值 $(变量名)

预定义变量

AR : 归档维护程序的名称,默认值为ar
CC : C 编译器的名称,默认值为cc
CXX : C++ 编译器的名称,默认值为g++
$@ : 目标的完整名称
$< : 第一个依赖文件的名称
$^ : 所有的依赖文件

makefile

src=$(patsubst %.c, %.o, $(wildcard *.c))
target=app
$(target):$(src)
    $(CC) $^ -o $@ -I include
%.o:%.c
    $(CC) $< -c -o $@ -I include
.PHONY:clean
clean:
    rm $(src) -f

$(wildcard PATTERN...)函数

功能:获取指定目录下指定类型的文件列表
参数:PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,用空格间隔
返回:得到的若干个文件的文件列表,文件名之间使用空格间隔
示例: $(wildcard *.c ./sub/*.c)
返回值格式: a.c b.c c.c d.c e.c f.c

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

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

<pattern>可以包括通配符%,表示任意长度的字串。
如果<replacement>中也包含%,那么,<replacement>中的这个%也是<pattern>中的那个%所代表的字串。(可以用\来转义,以\%来表示真实含义的%字符)
返回:函数返回被替换过后的字符串 示例:
$(patsubst %.c, %.o, x.c bar.c) 返回值: x.o bar.o

伪目标

.PHONY:clean   
clean:
	rm *.o -f

伪目标不会生成文件,这样防止目录中有一个clean文件,而会认为clean不需要更新(因为没有依赖)从而不执行规则 root#: make clean

gdb

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

启动和退出

root#: gdb a.out
(gdb) q

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

(gdb) set args 10 20
(gdb) show args

查看当前文件代码

(gdb) l (从默认位置显示)
(gdb) l 行号 (从指定的行显示)
(gdb) l 函数名(从指定的函数显示)

查看非当前文件代码

(gdb) l 文件名:行号
(gdb) l 文件名:函数名 文件名都是可以带路径的

设置显示的行数

(gdb) show list/listsize
(gdb) set list/listsize 行数

设置断点

(gbd) b 行号
(gdb) b 函数名
(gdb) b 文件名:行号
(gdb) b 文件名:函数

查看断点

(gdb) i b

删除断点

(gdb) d 断点编号

设置断点无效

(gdb) dis 断点编号

设置断点生效

(gdb) ena 断点编号

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

(gdb) b 10 if i==5

运行GDB程序

(gdb) start(程序停在第一行) (gdb) run(遇到断点才停)

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

(gdb) c

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

(gdb) n

变量操作

(gdb) p 变量名(打印变量值)
(gdb) ptype 变量名(打印变量类型)

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

(gdb) s
(gdb) finish(跳出函数体,函数体不能有断点)

自动变量操作

(gdb) display 变量名(自动打印指定变量的值)
(gdb) i/info display
(gdb) undisplay 编号

其它操作

(gdb) set var 变量名=变量值 (循环中用的较多)
(gdb) until (跳出循环, 循环中不能有断点)

core

ulimit -a 查看core打开没有
ulimit -c unlimited 默认core文件大小是0
gcc core.c -o coreapp -g
./coreapp
gdb coreapp
(gdb) core-file core 会显示程序崩溃信息