1.Linux系统编程入门

image-20220416095019121

  • GCC工作流程

    image-20220416095303150

  • GCC常用参数选项

    image-20220416095646595

    image-20220416103240998

    使用如:

    1
    2
    3
    
    gcc test.c -E -o test.i	#预处理源文件
    gcc test.i -S -o test.s # 编译经过预处理的源文件test.i,但不进行汇编
    gcc test.s -s -o a
    
  • 两种库文件:静态库动态库(共享库)。区别:

    • 静态库在程序的链接阶段被复制到程序中;
    • 动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。

    库的好处:1)代码保密;2)方便部署和分发。

  • 静态库的制作

    • 命名规则:

      • Linux:libxxx.a

        • lib : 前缀(固定)
        • xxx : 库的名字,自己起
        • .a : 后缀(固定)
      • Windows:libxxx.lib

    • 静态库制作

      • gcc获得.o文件; gcc -c xxx.c -I 路径

      • 将.o文件打包,使用ar工具(archive)

        ar rcs libxxx.a xxx.o xxx.o

        • r 将文件插入备存文件中;
        • c 建立备存文件;
        • s 索引。
  • 静态库的使用

    • gcc main.c -o app -I ./include/ -L ./lib/ -l 库名称
    • -I 头文件所在目录
    • -l 搜索使用的库名称
    • -L 库路径
  • 动态库的制作和使用

    • 制作

      • 命名规则:

        • Linux:libxxx.so

          • lib: 固定前缀
          • xxx : 库的名字,自己起
          • .so :固定后缀

          在Linux下是一个可执行文件

        • Windows:libxxx.dll

      • 动态库的制作

        • gcc得到.o文件,得到和位置无关的代码

          gcc -c -fpic/-fPIC a.c b.c

        • gcc 得到动态库

          gcc -shared a.o b.o -o libcalc.so

  • 静态库 与 动态库的工作原理

    • 静态库:GCC进行链接时,会把静态库中的代码打包到可执行程序中

    • 动态库:GCC进行链接时,动态库的代码不会被打包到可执行程序

    • 程序启动之后,动态库会被动态加载到内存中,通过 **ldd(list dynamic dependencies)**命令检查动态库依赖关系

    • 如何定位共享库文件呢?

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

  • 静态库 和 动态库 的对比

    • 程序编译成可执行程序的过程

      image-20220416153939567

    • 静态库制作过程

      image-20220416154128135

    • 动态库制作过程 (-fpic/-fPIC 生成与位置无关的目标代码

      image-20220416154238813

    • 静态库的优缺点

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

      image-20220416154745774

    • 动态库的优缺点

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

      image-20220416155035110

  • Makefile

    Makefile文件定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至进行更复杂的功能操作,因为Makefile文件就像一个Shell脚本一样,也可以执行操作系统的命令。

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

    • Makefile文件命名和规则

      • 文件命名

        • makefile或者Makefile
      • Makefile 规则

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

          目标 … : 依赖 …

          命令(shell命令)

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

    • 工作原理

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

        • 如果存在,执行命令;
        • 如果不存在,向下检查其他的规则,检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该规则中的命令
      • 检查更新,在执行规则中的命令时,会比较目标和依赖文件的时间;

        • 如果依赖的时间比目标的时间需要重新生成目标
        • 如果依赖的时间比目标的时间早,目标不需要重新更新,对应规则中的命令不需要被执行。
    • 变量

      • 自定义变量

        • 变量名 = 变量值 var = hello
      • 预定义变量

        AR :归档维护程序的名称,默认值为ar

        CC:C编译器的名称,默认值为cc

        CXX:C++编译器的名称,默认值为g++

        $@:目标的完整名称 # 自动变量只能在规则的命令中使用

        $<:第一个依赖文件的名称

        $^:所有的依赖文件

      • 获取变量的值

        $(变量名)

        image-20220417102016131

    • 模式匹配

      image-20220417102353020

    • 函数

      • $(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

      .PHONY 尾目标,可以用于最后的clean目标,用于删除 .o 目标文件

  • GDB调试

    • 什么是GDB

      一般来说,GDB主要帮助完成下面四个方面的功能:

      1. 启动程序,可以按照自定义的要求随心所欲的运行程序
      2. 可让被调试的程序在所指定的位置的断点处停住(断点可以是条件表达式)
      3. 当程序被停住时,可以检查此时程序中所发生的事;
      4. 可以改变程序,将一个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行数

    • 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

    • GDB命令——调试命令

      • 运行GDB程序

        start (程序停在第一行)

        run(遇到断点才停)

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

        c/continue

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

        n/next

      • 变量操作

        p/print 变量名(打印变量值)

        ptype 变量名(打印变量类型)

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

        s/step

        finish(跳出函数体)

      • 自动变量操作

        display num(自动打印变量的值)

        i/info display

        undisplay 编号

      • 其他操作

        set var 变量名=变量值

        until (跳出循环)

  • 文件IO

    • 标准C库IO函数 (可以跨平台)

      image-20220420212556936

    • 标准C库IO和Linux系统IO的关系 (调用和被调用)

      image-20220420213652006

    • 虚拟地址空间

      image-20220420214211970

      MMU(内存管理单元)将虚拟地址映射到物理地址。

    • 文件描述符

      image-20220420215257544

    • Linux 系统IO函数

      标准C库的 API,使用 man 3 函数名查看,Linux 系统IO函数,使用man 2 函数名查看说明。

      1
      2
      3
      4
      5
      6
      
      int open(const char *pathname, int flags);
      int open(const char *pathname, int flags, mode_t mode);
      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);
      
    • stat结构体

      image-20220421212627185

    • st_mode变量

      image-20220421212801173

  • 模拟实现 ls -l命令 (见lesson12的 ls-l.c

  • 文件属性操作函数

    1
    2
    3
    4
    
    int access(const char *pathname, int mode);
    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);	// 缩减或扩展文件的尺寸至指定的大小
    
  • 目录操作函数

    1
    2
    3
    4
    5
    
    int mkdir(const char *pathname, mode_t mode);
    int rmdir(const char *pathname);
    int rename(const char *oldpath, const char *newpath);
    int chdir(const char *path);
    char *getcwd(char *buf, size_t size);
    
  • 目录遍历函数

1
2
3
DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);

dirent结构体和d_type

image-20220422105027384

  • dup、dup2函数

    1
    2
    
    int dup(int oldfd);	// 复制文件描述符
    int dup2(int oldfd, int newfd);	// 重定向文件描述符
    
  • fcntl函数

    1
    2
    3
    
    int fcntl(int fd, int cmd, ... /* arg */);
    // 复制文件描述符,
    // 设置/获取文件的状态标志
    

2.Linux多进程开发

  • 进程概述

    • 程序和进程

      • 程序(占用磁盘大小,不占用内存和CPU资源)是 包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程。(二进制格式标识、机器语言指令、程序入口地址、数据、符号表及重定位表、共享库和动态链接信息 和 其他信息。)
      • 进程(占用内存资源和CPU资源) 是正在运行的程序的实例。
    • 单道、多道程序设计

      • 单道程序,即在计算机内存中只允许一个程序运行;
      • 引入多道程序设计技术的 根本目的为了提高CPU的利用率。
    • 时间片(timeslice)

      • 又称为 “量子(quantum)”或“处理器片(processor slice)” 是操作系统分配给每个正在运行的微观上的一段CPU时间。
      • 时间片 由 操作系统内核的调度程序分配给每个进程。
    • 并行和并发

    • 进程控制块(PCB)

      • Linux内核的进程控制块是 task_struct 结构体。

      • 其内部成员(需要掌握的)有:

        image-20220423150234361

      • 当前工作目录

      • umask掩码

      • 文件描述符表,包含很多指向file结构体的指针

      • 和信号相关的信息

      • 用户id和组id

      • 会话(session)和进程组

      • 进程可以使用的资源上限(ulimit -a

  • 进程状态转换

    • 进程的状态

      • 在三态模型中,分为三个基本状态:就绪态、运行态、阻塞态。

        image-20220423154753716

      • 在五态模型中,分为新建态、就绪态、运行态、阻塞态、终止态。

        image-20220427094737444

    • 进程相关命令

      • 查看进程

        • ps aux / ajx

          a:显示终端上的所有进程,包括其他用户的进程

          u:显示进程的详细信息

          x:显示没有控制终端的进程

          j:列出与作业控制相关的信息

          image-20220427101042933

        • STAT参数意义

          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 根据进程名杀死进程

          image-20220427102217992

    • 进程号和相关函数

      image-20220427102606767

  • 进程创建

    • 系统允许一个进程创建子进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型

      1
      2
      3
      
      #include <sys/types.h>
      #include <unistd.h>
      pid_t fork(void);
      

    父子进程指定代码的情况:

    image-20220427105213977

  • 父子进程虚拟地址空间情况

    image-20220427105840251

    父子进程中的变量在各自的栈空间中,互不影响。

    读时共享父进程虚拟地址空间,写时拷贝

    image-20220427110620205

  • 父子进程关系以及GDB多进程调试

  • 父子进程关系

    image-20220427111326013

  • GDB多进程调试(面试考 调试命令)

    使用GDB调试时,GDB默认只能跟踪一个进程,可以在fork函数调用之前,通过指令设置GDB调试工具跟踪父进程或子进程,默认跟踪父进程。

    设置调试父进程或子进程: set follow-fork-mode [parent(默认) | child]

    设置调试模式: set detach-on-fork [on | off]

    默认为on,表示调试当前进程时,其他的进程继续运行;如果为off,调试当前进程时,其他进程被GDB挂起。

    查看调试的进程:info inferiors

    切换当前调试的进程:inferior id

    使进程脱离GDB调试:detach inferiors id

  • exec函数簇

    • 作用:根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部内执行一个可执行文件。

    • exec函数簇的函数执行册成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,他们才会返回-1,从原程序的调用点接着往下执行。

      image-20220427194028639

      image-20220427194202915

  • 进程退出、孤儿进程、僵尸进程

    • 进程退出

      image-20220427200814249

    • 孤儿进程

      • 父进程运行结束,但子进程还在运行(未运行结束),这样的子进程称为 孤儿进程(Orphan Process)
      • 每当出现一个孤儿进程的时候,内核把孤儿进程的父进程设置为 init, 而init进程会循环地 wait() 它的已退出地子进程。这样,当一个孤儿进程凄凉地结束其声明周期地时候,init进程就会代表党和政府出面处理它地一切善后工作。
      • 因此孤儿进程并不会有什么危害
    • 僵尸进程

      • 每个结束后,都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程去释放。
      • 进程终止时,父进程尚未回收,子进程残留资源(PCB)存放在内核中,变成僵尸(Zombie)进程。
      • 僵尸进程不能被 kill -9 杀死。
      • 这样会导致一个问题,如果父进程不调用wait() 或 waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
  • wait函数

    image-20220427212124418

    • 进程回收

      image-20220427203725270

      wait() 和 waitpid() 函数的功能一样,区别在于,wait()函数会阻塞,waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束。

      注意:一次wait或waitpid 调用只能清理一个子进程,清理多个子进程应使用循环。

    • 退出信息相关宏函数

      image-20220427205601479

  • waitpid函数

    image-20220427212106011

  • 进程间通信简介

    image-20220427212215193

    • 进程间通信(IPC,Inter Processes Communication)的目的:

      • 数据传输:一个进程需要将它的数据发送给另一个进程;
      • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(他们)发生了某种事件(如进程终止时要通知父进程)。
      • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
      • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
    • Linux进程间通信的方式

      image-20220427212921082

  • 匿名管道概述

    • 匿名管道

      • 管道也叫 无名(匿名)管道,它是UNIX系统IPC的最古老形式,所有的UNIX系统都支持这种通信机制。
      • 统计一个目录中文件的数目命令:ls | wc -l,为了执行改命令,shell 创建了两个进程来分别执行 lswc

      image-20220427213246517

    • 管道的特点

      image-20220427213754334

      image-20220427214129774

    • 为什么可以使用管道进行进程间通信?

      因为 父子进程 共享文件描述符。

      image-20220427214457130

    • 管道的数据结构

      循环队列 image-20220427214652241

    • 匿名管道的使用

      • 创建匿名管道

        1
        2
        
        #include <unistd.h>
        int pipe(int pipefd[2]);
        
      • 查看管道缓冲大小命令

        ulimit -a

      • 查看管道缓冲大小函数

        1
        2
        
        #include <unistd.h>
        long fpathconf(int fd, int name);
        
  • 父子进程通过匿名管道通信

  • 匿名管道通信案例

  • 管道的读写特点和管道设置为非阻塞

  • 有名管道介绍及使用

  • 有名管道实现简单版聊天功能

3.Linux多线程开发

  • 线程概述

    • 概述
      • 进程是 CPU **分配资源的最小单位,**线程是 操作系统调度执行的最小单位。
      • 线程是轻量级的进程(LWP:Light Weight Process),在Linux环境下线程的本质仍是进程。
      • 查看指定进程的LWP号: ps -Lf pid
    • 线程之间共享和非共享资源
      • 共享资源
        • 进程ID和父进程ID
        • 进程组ID和会话ID
        • 用户ID和用户组ID
        • 文件描述符表
        • 信号处置
        • 文件系统的相关信息:文件权限掩码(umask)、当前工作目录
        • 虚拟地址空间**(除栈、.text)**
      • 非共享资源
        • 线程ID
        • 信号掩码
        • 线程特有数据
        • error变量
        • 实时调度策略和优先级
        • 栈,本地变量和函数的调用链接信息
  • 创建线程

    • 1
      2
      
      int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
      
      
  • 终止线程

  • 连接已终止的线程

  • 线程的分离

  • 线程取消

  • 线程属性

  • 线程同步

  • 互斥锁

  • 死锁

  • 读写锁

  • 生产者和消费者模型

  • 条件变量

  • 信号量

4.Linux网络编程

  • 网络结构模式

    • C/S结构
    • B/S结构
  • MAC地址、IP地址、端口

  • 网络模型

    • image-20220429145118491
    • image-20220429145129135
  • 协议

  • 网络通信的过程

  • socket介绍

  • 字节序

  • 字节序转换函数

  • socket地址

  • IP地址转换函数

  • TCP通信流程

  • socket函数

  • TCP通信实现(服务端)

  • TCP通信实现(客户端)

  • TCP三次握手

  • 滑动窗口

  • TCP四次握手

  • 多进程实现并发服务器

  • 多线程实现并发服务器

  • TCP状态转换

  • 半关闭、端口复用

  • IO多路复用简介

    • I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的系统调用主要有 select、poll 和 epoll。
  • select API介绍

    • select

      select工作过程分析:image-20220428154530510

      主旨思想:

      1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
      2. 调用一个系统函数(select),监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回。 a.这个函数是阻塞 b.函数对文件描述符的检测的操作是由内核完成
      3. 在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作。
       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
      
      // sizeof(fd_set) = 128  1024位
      #include <sys/time.h>
      #include <sys/types.h>
      #include <unistd.h>
      #include <sys/select.h>
      int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
      	- 参数:
              - nfds : 委托内核检测的最大文件描述符的值 + 1
              - readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
                      - 一般检测读操作
                      - 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
                      - 是一个传入传出参数
              - writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
                      - 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)
              - exceptfds : 检测发生异常的文件描述符的集合
              - timeout : 设置的超时时间
                  struct timeval {
                      long tv_sec; /* seconds */
                      long tv_usec; /* microseconds */
                  };
                  - NULL : 永久阻塞,直到检测到了文件描述符有变化
                  - tv_sec = 0 tv_usec = 0 不阻塞
                  - tv_sec > 0 tv_usec > 0 阻塞对应的时间
          - 返回值 :
              - -1 : 失败
              - >0(n) : 检测的集合中有n个文件描述符发生了变化
      
      // 将参数文件描述符fd对应的标志位设置为0
      void FD_CLR(int fd, fd_set *set);
      // 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
      int FD_ISSET(int fd, fd_set *set);
      // 将参数文件描述符fd 对应的标志位,设置为1
      void FD_SET(int fd, fd_set *set);
      // fd_set一共有1024 bit, 全部初始化为0
      void FD_ZERO(fd_set *set);			
      

      select缺点

      image-20220428154617449

  • select代码编写

  • poll API介绍以及代码编写

    • poll

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      
      #include <poll.h>
      struct pollfd {
          int fd; /* 委托内核检测的文件描述符 */
          short events; /* 委托内核检测文件描述符的什么事件 */
          short revents; /* 文件描述符实际发生的事件 */
      };
      struct pollfd myfd;
      myfd.fd = 5;
      myfd.events = POLLIN | POLLOUT;
      int poll(struct pollfd *fds, nfds_t nfds, int timeout);
      - 参数:
          - fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
          - nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1
          - timeout : 阻塞时长
                  0 : 不阻塞
                  -1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
                  >0 : 阻塞的时长
       - 返回值:
          -1 : 失败
          >0n : 成功,n表示检测到集合中有n个文件描述符发生变化
      

      image-20220428151755532

  • epoll API介绍

    image-20220428154419741

    • epoll

       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
      
      #include <sys/epoll.h>
      // 创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)。
      int epoll_create(int size);
      - 参数:
      	size : 目前没有意义了。随便写一个数,必须大于0
      - 返回值:
          -1 : 失败
          > 0 : 文件描述符,操作epoll实例的
      
      typedef union epoll_data {
          void *ptr;
          int fd;
          uint32_t u32;
          uint64_t u64;
      } epoll_data_t;
      
      struct epoll_event {
          uint32_t events; /* Epoll events */
          epoll_data_t data; /* User data variable */
      };
      
      常见的Epoll检测事件
      - EPOLLIN
      - EPOLLOUT
      - EPOLLERR
      
      // 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
      int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
      - 参数:
          - epfd : epoll实例对应的文件描述符
          - op : 要进行什么操作
              EPOLL_CTL_ADD: 添加
              EPOLL_CTL_MOD: 修改
              EPOLL_CTL_DEL: 删除
          - fd : 要检测的文件描述符
          - event : 检测文件描述符什么事情
      
      // 检测函数
      int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int
      timeout);
      - 参数:
          - epfd : epoll实例对应的文件描述符
          - events : 传出参数,保存了发送了变化的文件描述符的信息
          - maxevents : 第二个参数结构体数组的大小
          - timeout : 阻塞时间
              - 0 : 不阻塞
              - -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
              - > 0 : 阻塞的时长(毫秒)
      - 返回值:
          - 成功,返回发送变化的文件描述符的个数 > 0
          - 失败 -1
      
  • epoll代码编写

  • epoll的两种工作模式

    • LT 模式 (水平触发)

      假设委托内核检测读事件 -> 检测fd的读缓冲区 读缓冲区有数据 - > epoll检测到了会给用户通知 a.用户不读数据,数据一直在缓冲区,epoll 会一直通知 b.用户只读了一部分数据,epoll会通知 c.缓冲区的数据读完了,不通知

      LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的。

    • ET 模式(边沿触发)

      假设委托内核检测读事件 -> 检测fd的读缓冲区 读缓冲区有数据 - > epoll检测到了会给用户通知 a.用户不读数据,数据一致在缓冲区中,epoll下次检测的时候就不通知了 b.用户只读了一部分数据,epoll不通知 c.缓冲区的数据读完了,不通知

      ET(edge - triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

  • UDP通信实现

  • 广播

  • 组播

  • 本地套接字通信

5.项目实战与总结

  • 阻塞和非阻塞、同步和异步
  • Unix、Linux上的五种IO模型
  • Web服务器简介以及HTTP协议
  • 服务器编程基本框架和两种高效的事件处理模式

参考资料