Nowcoder Linux


linux高性能服务器编程

查询接口的网址

入门

  • 提问
    • image-20230817214308019
  • gcc编译过程
    • image-20230818151825613
    • 汇编
    • image-20230818151858659
      • -D 在编译时给程序指定一个宏
    • image-20230818154854334
      *
    • gcc和g++的区别
      • 都是GNU(组织) 的一个编译器
      • 两者都可以编译c和c++程序
        • 后缀.c的gcc把它当做是c代码,而g++当成cpp代码
        • 后缀.cpp两者都会认为是c++程序
        • 编译阶段g++会调用gcc,因为gcc命令不能自动和c++程序使用的库连接,所以通常用g++来完成链接,所以之后统一用g++
      • image-20230818153538141
      • image-20230818153815848

静态库的制作

  • 库文件是计算机的一类文件,可以简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量

  • image-20230818160105607

    • 编写库文件和编写一般的程序区别不大,只是库不能单独运行
  • 制作静态库

    • 命名规则:
      • linux:libxxx.a
        • lib:前缀(固定)
        • xxx:库的名字,自己起
        • .a :后缀(固定)
      • windows:libxxx.lib
    • 步骤
      • gcc获得.0文件
      • 将其打包,使用ar工具
    • image-20230818160851521
  • 制作过程

    • image-20230818161436745
    • gcc -c 生成 .o文件
  • image-20230818161516481

    • 使用 ar rcs命令生成静态库
  • 静态库的使用

    • 把库和库所依赖的头文件都要给使用者
    • 注意gcc编译时找的是同级的目录,怎么解决
      • 使用 –I
        • -l 后跟要查找的目录
    • -l
      • 指定要使用哪个库
    • image-20230818190830722
    • image-20230818190916189
      • -L 查找静态库的位置
  • 最终使用实例

    • image-20230818191405222
      • -L 指定静态库路径
      • -l 指定静态库名称
      • 注意:静态库名称不需要前缀lib和后缀.a

动态库的制作和使用

  • 命名规则
    • linux:libxxx.so
      • 前缀:lib (固定)
      • 后缀:.so(固定)
      • 在linux下是一个可执行文件
    • windows:libxxx.dll
  • 制作过程
    • gcc 得到.o文件,得到和位置无关的代码
    • gcc -c -fpic a.c b.c
    • gcc得到动态库
    • gcc -shared a.o b.o -o libcalc.so
  • image-20230818193442741
    • 加上参数-fpic得到与位置无关的路径
  • 使用示例
    • image-20230818194524227
      • 绿色文件代表可执行文件
    • image-20230818194654659
    • image-20230818194909297
      • 注意,按静态库的方法编译出来,会报错。动态库找不到对应的文件
    • 动态库加载原理和解决方案
      • 静态库工作原理
        • GCC进行链接时,会把静态库中代码打包到可执行程序中
      • 动态库:
        • GCC进行链接时,动态库的代码不会被打包到可执行程序中
        • 程序启动之后,动态库会被动加载到内存中,通过ldd(list dynamic dependencies)命令检查动态库依赖关系
        • image-20230818201935305
        • 以上是动态库如何载入内存中的先后顺序,在以上的路径之一中加入动态库的绝对路径就行
        • 配置LD_LIBRARY_PATH中的动态库的绝对位置
        • 终端级配置
          • export LD_LIBRARY_PATH = $LD_LIBRARY_PATH:动态库的绝对路径
            • image-20230818203408344
          • 配置完成之后使用ldd查找命令,libcalc.so 显示出正确的绝对路径
            • image-20230818203512098
            • 不再是not found
          • export配置方式是终端中的配置,将终端关闭之后即消失
        • 用户级配置
          • 在 .bashrc文件中添加export LD_LIBRARY_PATH = $LD_LIBRARY_PATH:动态库的绝对路径
          • . .bashrc命令使配置生效
          • image-20230818204139005

静态库和动态库的总结和对比

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

    • image-20230818211921410
      • 静态库和动态库都是在链接阶段进行处理
  • 静态库的制作过程

    • image-20230818212303322
  • 动态库的制作过程

    • image-20230818212422016
  • 静态库的优缺点

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

    image-20230818213423747

  • 动态库的优缺点

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

    image-20230818213906998

makefile

概述

  • makefile文件定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后变异,哪些文件需要重新编译。

  • makefile文件就像一个shell脚本一样,也可以执行操作系统的命令

  • 好处:

    • 自动化编译
    • 一旦写好,只需要一个make命令,整个工程就可以完全自动编译,极大的提高了软件开发的效率
  • 规则

    • image-20230820115900022
      • makefile中可以有其他的规则,其他规则都是为上面的规则服务的
  • 使用

    • image-20230820120601055

    • 工作原理

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

        • 如果存在执行命令
        • 如果不存在,向下检查其他的规则,检查有没有一个规则是用来生成这个依赖的,如果找到了则执行该规则中的命令

        image-20230820153023845

        • 注意:下面的规则都是默认为第一条规则服务,若第一条规则中没有出现,那么有些命令不会执行
      • 检查更新,在执行规则中的命令时,会比较目标和依赖文件的时间

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

      image-20230820153800782

      • image-20230820154132957

变量

  • image-20230820154434059

  • 自定义变量

    • var = hello

    • 预定义变量

    • image-20230820154657333

    • image-20230820155235868

      • 简化版本如上图
    • 模式匹配法

      • image-20230820155306513
      • image-20230820162653437
    • 函数**$(wildcard PATTERN …)**

      • $(wildcard PATTERN …)

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

        image-20230820182626111

  • $(patsubst , ,)

    • 功能:查找中的单词(单词以”空格”、”Tab”或”回车””换行”分割)是否符合,如果匹配的话,则以替换
    • 可以包括通配符%,表示任意长度的字符串。如果中也包含%,那么中的这个%将是中的那个%所代表的字串
    • 返回:函数返回被替代过后的字符串

    image-20230820183919220

    • 示例

    image-20230821143251056

    • 注意!patsubst不能有空格,否则会将.c文件一起删除!!!

GDB调试

  • GDB是由GUN提供的调试工具,同GCC配套组成了一套完整的开发环境,GDB是Linux和许多类U你下系统中的标准开发环境

  • 完成的四大功能

    • 启动程序,可以按照自定义的要求随心所欲的运行程序
    • 可以让调试的程序在所指定的调置的断点处停住(断点可以是条件表达式)
    • 当程序被停住时,可以检查此时程序中所发生的事
    • 可以改变程序,将一个bug产生的影响修正从而测试其他bug

    image-20230821150714851

  • 调试准备

    • image-20230821151302221
  • GDB的命令

    • 基本操作
      • image-20230821151316149
        • 使用GDB list命令查看
    • 断点操作
      • image-20230821160926177
        • image-20230821161638258
          • 删除断点
        • 设置断点不可用
          • image-20230821162002872
          • disable + number
        • 设置条件断点
          • image-20230821162210161
      • 启动GDB程序、调试命令
        • image-20230821185038336
          • 注意start和run的区别
          • 想要跳过循环,必须将程序内部的断点先删除掉,然后再循环判断起始跳过

标准c库io函数和Linux系统io函数对比

  • 从内存的角度考虑IO

  • 两者是调用和被调用的关系

    • c标准库调用内核函数

    • image-20230821195822872

    • 文件描述符:指定打开的文件,定位文件

    • 文件读写指针:读取和写数据,内部维护两个指针

    • I/O缓冲区:提高执行的效率。当缓冲区慢了,系统调用读写程序

      • 什么时候将缓冲区内容写到瓷盘

        image-20230821195430624

        • 缓冲区默认字节8192B

虚拟地址空间

  • 虚拟地址空间是不存在的
  • 虚拟内存会被mmu映射到实际的内存中
  • 分为用户区和内核区
    • image-20230824203318343
      • NULL和nullptr 都在受保护的地址中
      • 堆空间要比栈空间大。堆空间从下往上,栈空间从上往下
      • 系统调用:调用Linux系统中的API
        • 通过系统调用对内核进行操作
        • 内核区:
          • 内存管理
          • 进程管理
          • 设备驱动管理
          • 文件系统

文件描述符

  • 文件描述符在进程的内核区
    • 内核区中有PCB(一个复杂的结构体),由进程控制块管理进程
    • PCB中有一个数组,叫做文件描述符表,默认大小是1024.最多同时打开1024个文件
      • 里面可以存储多个文件描述符
      • 可以同时打开多个文件,一个文件对应一个文件描述符
    • 文件描述符表
      • image-20230824205128646
      • 前三个默认为打开状态,对应于当前终端
      • 一个文件可以同时被打开多次,对应多个文件描述符,每个文件描述符是不同的

open

打开文件

  • perror的具体用法

    • image-20230824212328776
  • open总结

    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
    /*
    #include <fcntl.h>
    #include <sys/stat.h>
    #include <sys/types.h>

    //打开一个已经存在的文件
    参数:
    -pathname:要打开的文件路径
    -flags must include one of the following
    access modes: O_RDONLY, O_WRONLY, or O_RDWR 这三个操作是互斥的
    返回值:
    -是文件描述符,若定位失败返回 -1
    errno:属于Linux系统函数库,库里面的一个全局变量,记录的是最近的错误号
    perror:
    int open(const char *pathname, int flags);

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

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

    */

创建新文件

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
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

/*
//创建一个文件
int open(const char *pathname, int flags, mode_t mode);
参数
-pathname:要创建的文件的路径
-flags:对文件的操作权限和其他的设置
是一个int类型的数据,占4个字节,32位,每一位就是一个标志位
必选项:O_RDONLY, O_WRONLY, or O_RDWR 三选一
可选项:O_CREATE 文件不存在,创建新文件
-mode:八进制的数,表示用户创建出的新的操作权限
最终的权限是 mode & ~umask umask = 0022 ~umask = 0755
0777 -> 111111111
0755 -> 111101101 两者相与 umask作用是抹去某些权限

*/

int main() {
// 创建一个新文件
int fd = open("create.txt", O_RDWR | O_CREAT, 0777);
if (fd == -1) {
perror("open");
}
// 关闭文件描述符
close(fd);
return 0;
}

read、write函数

  • 系统中的IO函数

    • image-20230825113414291
  • 具体使用

    • /*
          #include <unistd.h>
          参数
              - fd 文件描述符 通过文件描述符操作某一个文件
              - buf 需要读取数据存放的地方,一般是数组的地址 ,是缓冲区(传出参数)
              - count 指定的数组的大小
      
          返回值
              - 成功:
                  >0 : 返回实际的读取到的字节数
                  =0 : 文件已经读取完了
              - 失败
                  -1 : 文件读取失败
          ssize_t read(int fd, void buf[.count], size_t count);
      
      
          #include <unistd.h>
          参数
              - fd 文件描述符 通过文件描述符操作某一个文件,一般通过open得到
              - buf 写入数据的地方,一般是数组的地址 ,是缓冲区(传出参数)
              - count 要写的数据的的实际的大小
          返回值
              - 成功:
                  >0 : 返回实际的写入的字节数
                  =0 : 没有任何内容写入
              - 失败
                  -1 : 文件写入失败
          ssize_t write(int fd, const void buf[.count], size_t count);
      
      */
      
      #include <fcntl.h>
      #include <stdio.h>
      #include <sys/stat.h>
      #include <sys/types.h>
      #include <unistd.h>
      
      int main() {
          // 通过open打开文件
          int srcfd = open("english.txt", O_RDONLY);
          if (srcfd == -1) {
              perror("open");
              return -1;
          }
      
          // 创建一个新的文件(拷贝文件)
          int destfd = open("cpy.txt", O_WRONLY | O_CREAT, 0664);
          if (destfd == -1) {
              perror("copy");
              return -1;
          }
          // 频繁的读写数据
          char buf[1024] = {0};
          int len = 0;
          while ((len = read(srcfd, buf, sizeof(buf))) > 0) {
              write(destfd, buf, len);
          }
          // 关闭文件
          close(destfd);
          close(srcfd);
      
          return 0;
      }
      
      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



      ### lseek函数

      ~~~c
      /*
      #include <unistd.h>
      参数
      - fd 文件描述符
      - offset 偏移量
      - whence
      SEEK_SET
      The file offset is set to offset bytes.
      设置文件指针的偏移量

      SEEK_CUR
      The file offset is set to its current location plus offset bytes.
      设置偏移量:当前位置 + 第二个参数offset的值

      SEEK_END
      The file offset is set to the size of the file plus offset bytes.
      设置偏移量:文件大小 + 第二个offset的值
      返回值:返回文件指针的位置
      off_t lseek(int fd, off_t offset, int whence);
      函数的作用
      1.移动文件指针到头文件
      lseek(fd,0,SEEK_SET);
      2.获取当前文件指针的位置
      lseek(fd,0,SEEK_CUR);
      3.获取文件长度
      lseek(fd,0,SEEK_END);
      4.拓展文件的长度,当前文件10b,增加100字节
      lseek(fd,100,SEEK_END);

      */
      #include <stdio.h>
      #include <unistd.h>
      #include <sys/types.h>
      #include <sys/stat.h>
      #include <fcntl.h>

      int main(){
      int fd = open("hello.txt" , O_RDWR);
      if(fd == -1){
      perror("open");
      return -1;
      }
      int ret = lseek(fd,100,SEEK_END);
      if(ret == -1){
      perror("lseek");
      return -1;
      }
      //写入空数据
      write(fd , " " , 1);
      close(fd);
      return 0;
      }
    • 用途

      • 拓展文件,提前将文件大小拓展出来,占用好。避免内存不够的情景
      • 注意文件拓展要进行一次写入之后,拓展的大小才能生效

stat,lstat函数

  • stat结构体参数

    • image-20230825150650081

    • st_mode变量

      • image-20230825150852029
        • 如何查看权限
          • 进行操作
        • 判断文件类型
          • 与掩码进行操作
    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
    /*
    #include <sys/stat.h>
    参数
    - pathname 操作文件的路径
    - statbuf 结构体变量,传出参数,用于保存获取到的文件的信息
    返回值
    成功 0
    失败 -1 设置errno
    int stat(const char *restrict pathname,
    struct stat *restrict statbuf);
    作用:获取一个文件相关的一些信息
    int lstat(const char *restrict pathname,
    struct stat *restrict statbuf);
    与上一个函数的区别:该函数获取的是软连接文件的信息,而上一个函数获取的是原函数的信息

    */

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <unistd.h>
    #include <fcntl.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;
    }

模拟实现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
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <string.h>

// 模拟实现ls -l 指令 -rwxrwxrwx 1 yjx yjx 11 Aug 25 15:17 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] = '?';
}

// 判断文件的访问权限

// 文件所有者
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;
}

文件属性操作函数

  • image-20230825192118122

  • access

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

    #include <unistd.h>
    #include <stdio.h>

    int main(){
    int ret = access("a.txt" , F_OK);
    if(ret == -1){
    perror("access");
    return -1;
    }
    printf("文件存在\n");

    }
  • chmod

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

    #include <sys/stat.h>
    #include <stdio.h>

    int main(){
    int ret = chmod("a.txt",0775);
    if(ret == -1){
    perror("chmod");
    return -1;
    }

    printf("修改成功\n");\
    return 0;
    }
  • truncate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /*
    #include <unistd.h>
    缩减或者扩展文件的尺寸至指定的大小
    参数
    - path 需要修改的文件的路径
    - length 需要最终文件变成的大小
    int truncate(const char *path, off_t length);
    */
    #include <stdio.h>
    #include <unistd.h>

    int main() {
    int ret = truncate("b.txt", 5);
    if (ret == -1) {
    perror("truncate");
    return -1;
    }
    return 0;
    }

目录操作函数

  • image-20230825200219228

  • mkdir

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

    #include <sys/stat.h>
    #include <sys/types.h>
    #include <stdio.h>

    int main() {

    int ret = mkdir("aaa", 0777);

    if(ret == -1) {
    perror("mkdir");
    return -1;
    }

    return 0;
    }
  • rename

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /*
    #include <stdio.h>
    int rename(const char *oldpath, const char *newpath);

    */
    #include <stdio.h>

    int main() {

    int ret = rename("aaa", "bbb");

    if(ret == -1) {
    perror("rename");
    return -1;
    }

    return 0;
    }
  • chdir、getcwd

    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
    /*

    #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 <unistd.h>
    #include <stdio.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <fcntl.h>

    int main() {

    // 获取当前的工作目录
    char buf[128];
    getcwd(buf, sizeof(buf));
    printf("当前的工作目录是:%s\n", buf);

    // 修改工作目录
    int ret = chdir("/home/nowcoder/Linux/lesson13");
    if(ret == -1) {
    perror("chdir");
    return -1;
    }

    // 创建一个新的文件
    int fd = open("chdir.txt", O_CREAT | O_RDWR, 0664);
    if(fd == -1) {
    perror("open");
    return -1;
    }

    close(fd);

    // 获取当前的工作目录
    char buf1[128];
    getcwd(buf1, sizeof(buf1));
    printf("当前的工作目录是:%s\n", buf1);

    return 0;
    }

目录遍历函数

  • image-20230826142644407

  • dirent结构体和d_type

    • image-20230826144649236
      • d_reclen:文件的实际长度
    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
    /*
    // 打开一个目录
    #include <dirent.h>
    #include <sys/types.h>
    DIR *opendir(const char *name);
    参数:
    - name: 需要打开的目录的名称
    返回值:
    DIR * 类型, 理解为目录流
    错误返回NULL


    // 读取目录中的数据
    #include <dirent.h>
    struct dirent *readdir(DIR *dirp);
    - 参数:dirp是opendir返回的结果
    - 返回值:
    struct dirent,代表读取到的文件的信息
    读取到了末尾或者失败了,返回NULL

    // 关闭目录
    #include <dirent.h>
    #include <sys/types.h>
    int closedir(DIR *dirp);

    */
    #include <dirent.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/stat.h>
    #include <sys/types.h>

    // 读取某个目录下普通文件的个数
    int main(int argc, char* argv[]) {
    if (argc < 2) {
    printf("%s path\n", argv[0]);
    return -1;
    }
    int cnt = getFileNum(argv[1]);
    printf("普通文件的个数为: %d\n", cnt);
    return 0;
    }

    // 用于获取目录下所有普通文件的个数
    int getFileNum(const char* path) {
    // 1.打开目录
    DIR* dir = opendir(path);
    if (dir == NULL) {
    perror("opendir");
    exit(0);
    }

    // 2.读取文件
    // 记录普通话文件的个数
    int cnt = 0;
    struct dirent* ptr;
    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);
    cnt += getFileNum(newpath);
    }
    if (ptr->d_type == DT_REG) {
    // 普通文件
    cnt++;
    }
    }

    // 关闭目录
    closedir(dir);
    return cnt;
    }

dup、dup2函数

  • image-20230826152512290

    • dup复制文件描述符
    • dup2重定向文件描述符
  • dup

    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
    /*
    #include <unistd.h>
    作用 复制一个新的文件描述符 指向同一个文件
    参数
    oldfd: 原文件描述符
    int dup(int oldfd);
    */

    #include <fcntl.h>
    #include <stdio.h>
    #include <string.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <unistd.h>

    int main() {
    int fd = open("a.txt", O_RDWR | O_CREAT, 0664);
    int fd1 = dup(fd);
    if (fd1 == -1) {
    perror("dup");
    return -1;
    }
    printf("fd: %d , fd1: %d\n", fd, fd1);
    close(fd);
    char* str = "hello world";
    int ret = write(fd1, str ,strlen(str));
    if (ret == -1) {
    perror("write");
    return -1;
    }
    close(fd1);
    return 0;
    }
  • dup2

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

    #include <stdio.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>


    int main(){
    int fd = open("1.txt" , O_RDWR | O_CREAT , 0664);
    if(fd == -1){
    perror("open");
    return -1;
    }
    int fd1 = open("2.txt" , O_RDWR | O_CREAT, 0664);
    printf("fd: %d , fd1: %d\n",fd,fd1);

    int fd2 = dup2(fd,fd1);
    if(fd2 == -1){
    perror("dup2");
    return -1;
    }
    char * str = "hello world";
    //现在通过fd1去写数据,实际操作的是1.txt,而不是2.txt
    int ret = write(fd1,str,strlen(str));
    if (ret == -1)
    {
    perror("write");
    return -1;
    }
    printf("fd: %d , fd1: %d , fd2: %d\n",fd,fd1,fd2);


    }

fcntl函数

  • image-20230826161549697

    • 掌握如上两大功能
    • image-20230829201827539

多进程开发

进程概述

  • 什么是程序
    • image-20230827143121674
      • 程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程
  • 什么是进程
    • image-20230827143520324
      • 进程是正在运行的程序的实例
  • 单道、多道程序
    • image-20230827143957209
      • 从微观上看,任意时刻CPU上运行的程序只有一个
  • 时间片
    • image-20230827144228372
      • 将时间切片,操作系统分配给每个正在运行的进程围观上的一段CPU事件
      • 时间片通常非常短,用户不会感觉到
  • 并行和并发
    • image-20230827145200725
      • 并行是同一时刻有多条指令在多个处理器上同时执行
      • 并发是同一时刻只有一条指令执行
  • 进程控制块PCB
    • image-20230827145429923
      • 维护进程相关信息。
      • Linux内核的进程控制块是 tast_struct结构体
      • PCB中还有
        • image-20230827145629299

进程的状态转换

  • 进程的基本三个状态
    • image-20230827145913902
    • 五态模型
      • image-20230827151855151
  • 进程相关的命令
    • image-20230827152042220
    • STAT参数意义:
      • image-20230827152358884
    • 查看父进程id 用ps ajx
  • 显示进程动态信息
    • image-20230827152807943
  • 杀死进程
    • image-20230827153125362
  • 进程号和相关函数
    • image-20230827153334029
      • 进程号是唯一的,但是可以重复使用
      • 任何进程都有一个父进程(除了init进程)

进程创建

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

  • image-20230827160007365

  • fork()函数

    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>
    作用: 用于创建子进程
    返回值: fork的返回值会返回两次:一次是在父进程中,一次是在子进程中
    在父进程中返回创建的子进程的ID
    在子进程中返回0
    如何父进程和子进程,通过fork的值返回
    在父进程中返回-1表示创建子进程失败,冰鞋设置errno
    什么时候会创建失败呢?
    1.当前系统的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN
    2.系统内存不足时,这时errno的值被设置为ENOMEM
    pid_t fork(void);

    */

    #include <stdio.h>
    #include <string.h>
    #include <sys/types.h>
    #include <unistd.h>

    int main() {
    // 创建子进程
    pid_t pid = fork();
    // 判断是父进程还是子进程
    if (pid > 0) {
    printf("pid : %d\n", pid);
    // 当前是父进程,返回的是创建的子进程的进程号
    printf("i am parent process , pid : %d , ppid: %d\n", getpid(), getppid());

    } else if (pid == 0) {
    // 当前是子进程
    printf("i am child process , pid : %d , ppid : %d\n", getpid(), getppid());
    }
    for (int i = 0; i < 3; i++) {
    printf("i : %d\n", i);
    sleep(1);
    }

    return 0;
    }
    • 结果如图image-20230827163950099
      • 子父进程交替运行(时间片)
    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>
    作用: 用于创建子进程
    返回值: fork的返回值会返回两次:一次是在父进程中,一次是在子进程中
    在父进程中返回创建的子进程的ID
    在子进程中返回0
    如何父进程和子进程,通过fork的值返回
    在父进程中返回-1表示创建子进程失败,冰鞋设置errno
    什么时候会创建失败呢?
    1.当前系统的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN
    2.系统内存不足时,这时errno的值被设置为ENOMEM
    pid_t fork(void);

    */

    #include <stdio.h>
    #include <string.h>
    #include <sys/types.h>
    #include <unistd.h>

    int main() {
    // 创建子进程
    pid_t pid = fork();
    // 判断是父进程还是子进程
    if (pid > 0) {
    printf("pid : %d\n", pid);
    // 当前是父进程,返回的是创建的子进程的进程号
    printf("i am parent process , pid : %d , ppid: %d\n", getpid(), getppid());

    } else if (pid == 0) {
    // 当前是子进程
    printf("i am child process , pid : %d , ppid : %d\n", getpid(), getppid());
    }
    for (int i = 0; i < 3; i++) {
    printf("i : %d\n", i);
    sleep(1);
    }

    return 0;
    }

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

  • 父子进程执行代码情况

    • image-20230827201254167
  • 父子进程虚拟地址空间

    • 调用fork()函数相当于调用新的地址空间
    • 子进程的用户区数据和父进程一样(克隆过来的)
    • 内核区也会拷贝过来,但是pid不同。
    • fork()函数在父进程中执行,返回值pid是局部变量,在栈空间中。子进程不回执行fork()函数
    • 两者的内存空间内容相同,但是是不同的地址

    image-20230827201908769

Linux的fork函数是通过写时拷贝实现

写时拷贝是一种可以推迟甚至避免拷贝数据的技术

内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间

只用在需要写入的时候才会复制地址空间,从而使各个进程拥有各自的地址空间

  • 也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享
  • 注意!fork()之后是文件共享

总结

  • 父子进程之间的关系

    • 区别

    1.fork()函数返回值不同

    • 父进程中: > 0 返回的是子进程的id

    • 子进程中: == 0

    2.PCB中的一些数据不同

    • 当前进程的pid

    • 当前进程的ppid

    • 信号集

    • 共同点

    某些状态下: 子进程刚被创建出来,还没有执行任何写数据的操作

    - 用户区的数据是一样的
    -  文件描述符表
    
  • 父子进程对变量是不是共享的?

    刚开始的时候是一样的,是共享的。如果修改了数据,就不再共享

    读时共享,写时拷贝。

GBD多进程调试

  • image-20230827211500922
    • 使用 set follow-fork-mode 设置默认调试
    • set detach-on-fork设置调试模式
    • inferior id 切换调试的进程
    • datach inferiors id使进程脱离GDB调试

exec函数族

  • c语言中没有函数重载。一些列功能相同或相似的函数称为函数族

  • image-20230828092512320

    • 需要传递文件名来找到可执行文件。在调用进程内部执行一个可执行文件
      • 一般先用进程fork出子进程,在子进程中执行exec函数替代进程
    • 调用成功不返回,失败返回-1
  • 图解,当执行exec函数之后

    • image-20230828093025909
      • 两者已经交换了,但是id没变
      • 躯壳没变,内部变了
  • exec函数族

    • image-20230828093117034
      • 前6个函数是标准c库中的函数,execvve()是Linux中的系统API
    • 前两个用的最多
  • execl()函数

    • image-20230828095153161
    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
    /*
    #include <unistd.h>

    extern char **environ;
    参数
    - path: 需要指定的执行的文件的路径或者名称 推荐使用绝对路径
    - arg: 执行可执行文件所需要的参数列表
    第一个参数一般没有什么作用,为了方便,一般写执行程序的名称
    从第二个参数开始往后,就是程序执行所需要的参数列表
    参数最后需要以NULL结束(哨兵)
    返回值
    成功不返回(因为调用成功之后用户区已经交换,收取返回值也没有意义了)
    只有调用失败才会返回-1并且设置errno
    int execl(const char *pathname, const char *arg, ...);
    */
    #include <stdio.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <unistd.h>

    int main() {
    // 创建一个子进程,在子进程中执行exec函数族中的函数
    pid_t pid = fork();
    if (pid > 0) {
    printf("i am parent process, pid: %d\n", getpid());
    sleep(1);
    } else if (pid == 0) {
    execl("hello", "hello", NULL);
    printf("i am child process, pid = %d\n", getpid()); // 在子进程中这些代码都会被替代掉,不会执行,执行hello可执行文件中的代码
    }
    for (int i = 0; i < 3; i++) {
    printf("i = %d , pid = %d\n", i, getpid());
    }
    }
    • execl()也可以执行操作系统的shell文件
  • execlp()

    • 可以从环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
    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
    /*
    #include <unistd.h>
    int execlp(const char *file, const char *arg, ... );
    - 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
    - 参数:
    - file:需要执行的可执行文件的文件名
    a.out
    ps

    - arg:是执行可执行文件所需要的参数列表
    第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
    从第二个参数开始往后,就是程序执行所需要的的参数列表。
    参数最后需要以NULL结束(哨兵)

    - 返回值:
    只有当调用失败,才会有返回值,返回-1,并且设置errno
    如果调用成功,没有返回值。


    int execv(const char *path, char *const argv[]);
    argv是需要的参数的一个字符串数组
    char * argv[] = {"ps", "aux", NULL};
    execv("/bin/ps", argv);

    int execve(const char *filename, char *const argv[], char *const envp[]);
    char * envp[] = {"/home/nowcoder", "/home/bbb", "/home/aaa"};


    */
    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>

    int main() {
    // 创建一个子进程,在子进程中执行exec函数族中的函数
    pid_t pid = fork();

    if (pid > 0) {
    // 父进程
    printf("i am parent process, pid : %d\n", getpid());
    sleep(1);
    } else if (pid == 0) {
    // 子进程
    execlp("ps", "ps", "aux", NULL);
    perror("execlp");
    printf("i am child process, pid : %d\n", getpid());
    }

    for (int i = 0; i < 3; i++) {
    printf("i = %d, pid = %d\n", i, getpid());
    }

    return 0;
    }

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

  • 进程退出

    • image-20230828125257042
      • 子进程结束之后只能释放用户区的数据,内核区的数据需要父进程去回收
      • c库的exit()会多做两个事情,如上图
      • 注意:
        • 标准c库的exit()函数会刷新IO缓冲区,而linux系统中的_exit()不会
  • 孤儿进程

    • 父进程运行结束,但子进程还在运行。这样的子进程就称为孤儿进程
      • 当出现孤儿进程时,内核将把孤儿进程的父进程设置为init,而init进程会循环的wait()它的已经退出的子进程,这样,当一个孤儿进程结束了生命周期之后,init进程就会处理它。
      • 因此孤儿进程并不会有什么危害
      • init进程相当于它的干爹

    image-20230828141742090

  • 僵尸进程

    • 每个进程结束后,都会释放自己地址空间中的用户区数据,内核区的PCB没有办法自己释放掉,需要父进程去释放
    • 进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸进程
    • 僵尸集成不能被kill- 9杀死
    • image-20230828143059721
    • 僵尸进程是有危害的,父进程应该时候**wait()或者waitpid()**释放资源
    • 产生原因:
      • 子进程死了,但是父进程没有回收资源
  • image-20230828143505084

    • 可能导致新进程没有可用进程号可分配
  • 解决方法

    • 将父进程杀死,让僵尸进程变成孤儿进程交给init(进程号位1)进程托管

wait函数

  • 父进程回收子进程资源

    • image-20230828144306238
      • PCB中的信息内核不会释放,需要父进程使用wait()和waitpid()得到它的退出状态同时彻底清除掉这个进程
      • 一次调用只能清理一个子进程
  • wait()函数使用

    • 使用该函数的进程会挂起
    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
    /*
    #include <sys/wait.h>
    功能: 等待任意一个子进程结束,如果任意一个子进程结束了,此函数将会回收子进程资源
    参数:
    - wstatus: 进程退出时的状态信息,传入的是一个int类型的地址,传出参数
    返回值:
    成功返回被回收的子进程id
    失败返回-1(所有的子进程都结束了,调用函数失败)
    pid_t wait(int *_Nullable wstatus);
    调用wait()函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才能被唤醒(继续执行)
    如果没有子进程了,函数立刻返回,返回-1。如果子进程都结束了,也会立即返回-1

    */

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>

    int main() {
    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;
    for (int i = 0; i < 5; i++) {
    pid = fork(); // 注意这里不止产生5个,需要下面的判断
    if (pid == 0) {
    break;
    }
    }
    if (pid > 0) {
    // 父进程
    while (1) {
    printf("parent, pid = %d\n", getpid());
    int ret = wait(NULL);
    if (ret == -1) {
    break;
    }

    printf("child die , pid = %d\n", ret);
    sleep(1);
    }
    } else if (pid == 0) {
    // 子进程
    while (1) {
    printf("child, pid = %d\n", getpid());
    sleep(1);
    }
    }
    }
  • 退出信息相关宏函数

    • image-20230828152141644

      • 使用

        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
        /*
        #include <sys/wait.h>
        功能: 等待任意一个子进程结束,如果任意一个子进程结束了,此函数将会回收子进程资源
        参数:
        - wstatus: 进程退出时的状态信息,传入的是一个int类型的地址,传出参数
        返回值:
        成功返回被回收的子进程id
        失败返回-1(所有的子进程都结束了,调用函数失败)
        pid_t wait(int *_Nullable wstatus);
        调用wait()函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才能被唤醒(继续执行)
        如果没有子进程了,函数立刻返回,返回-1。如果子进程都结束了,也会立即返回-1

        */

        #include <stdio.h>
        #include <sys/types.h>
        #include <sys/wait.h>
        #include <unistd.h>
        #include <stdlib.h>

        int main() {
        // 有一个父进程,创建5个子进程(兄弟)
        pid_t pid;
        for (int i = 0; i < 5; i++) {
        pid = fork(); // 注意这里不止产生5个,需要下面的判断
        if (pid == 0) {
        break;
        }
        }
        if (pid > 0) {
        // 父进程
        while (1) {
        printf("parent, pid = %d\n", getpid());
        // int ret = wait(NULL);
        int st;
        int ret = wait(&st);
        if (ret == -1) {
        break;
        }

        if (WIFEXITED(st)) {
        // 正常退出
        printf("退出状态码: %d\n", WEXITSTATUS(st));
        }
        if (WIFSIGNALED(st)) {
        // 异常终止
        printf("被%d信号干掉了\n", WTERMSIG(st));
        }

        printf("child die , pid = %d\n", ret);
        sleep(1);
        }
        } else if (pid == 0) {
        // 子进程
        while (1) {
        printf("child, pid = %d\n", getpid());
        sleep(1);
        }
        exit(1);
        }
        return 0;
        }

waitpid函数

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
/*
#include <sys/wait.h>
功能:回收指定进程号的子进程,可以设置是否阻塞
参数:
- pid:
pid > 0 表示某个子进程的pid
pid = 0 回收当前进程组的所有子进程
pid = -1 表示回收所有的子进程,相当于wait()
pid < -1 某个进程组的组id,绝对值代表该进程组的id,回收指定进程组中的子进程
- wstatus : 进程退出时的状态信息,传入的是一个int类型的地址,传出参数
- options:
0: 阻塞
WNOHANG: 非阻塞
返回值
> 0 : 返回子进程的id
= 0 : options = WNOHANG, 表示还有子进程或者
= -1: 错误或者没有子进程了
pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options);
*/

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;

// 创建5个子进程
for (int i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) {
break;
}
}

if (pid > 0) {
// 父进程
while (1) {
printf("parent, pid = %d\n", getpid());

int st;
// int ret = waitpid(-1, &st, 0); // 阻塞的情况
int ret = waitpid(-1, &st, WNOHANG); // 非阻塞

if (ret == -1) {
break;
}
if (ret == 0) {
// 还有子进程存
printf("还有子进程存在\n");
sleep(1);
continue;
}
if (ret > 0) {
if (WIFEXITED(st)) {
// 是不是正常退出
printf("退出的状态码:%d\n", WEXITSTATUS(st));
}
if (WIFSIGNALED(st)) {
// 是不是异常终止
printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}

printf("child die, pid = %d\n", ret);
}
sleep(1);
}

} else if (pid == 0) {
// 子进程
while (1) {
printf("child, pid = %d\n", getpid());
sleep(1);
}

exit(0);
}

return 0; // exit(0)
}

进程间通信IPC

  • 进程是一个独立的资源分配,不同进程之间的资源是独立的,没有关联,不同的进程间不能之间访问
  • 进程通信的目的
    • 数据传输
    • 事件通知
    • 资源共享
    • 进程控制
    • image-20230828201621809

进程间的通信方式

  • 同一主机进程间通信

    • UNIX进程间通信方式
    • 匿名管道
    • 有名管道
    • 信号
    • System V进程间通信方式
    • 消息队列
    • 共享内存
    • 信号量
  • 不同主机(因特网)进程间通信

    Socket

匿名管道

匿名管道概述
  • 管道也叫无名(匿名)管道,它是UNIX系统IPC(进程间通信)的最古老形式,所有的UNIX系统都支持这种通信

  • image-20230828203304502

  • | 称为管道符

    • 理解:
      • ls指定得到的内容交给后面的指令进行统计
    • 实际:
      • 两个命令创建两个进程,**| 的作用是创建管道,进行通信**,把前面进程运行得到的内容发送给后面进程
  • 管道的特点

    • 管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同
    • 管道拥有文件的特质:读操作、写操作。匿名管道没有文件实体,有名管道有文件实体但不存储数据。可以按照操作文件的方式对管道进行操作
    • 一个管道是一个字节流,使用管道时不存在消息或消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块大小是多少
    • 通过管道传递的数据是顺序的,从管道中读取出来的字节顺序和它们被写入管道的顺序是完全一致的

    image-20230828205350998

    • 在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的
    • 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,无法使用lseek()
    • 匿名管道只能在具有公共祖先的进程之间使用后

    image-20230828211325363

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

    • 子进程fork()出来之后跟父进程共享文件描述符
    • image-20230828212604239
      • 注意:一定要是有关系的进程
  • 管道的数据结构

    • image-20230828212917829
      • 环形队列极大的节省了空间
  • 匿名管道的使用

    • image-20230828212940361
  • 一个进程发一个进程接收

    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
    /*
    #include <unistd.h>
    功能: 创建一个匿名管道用来进程间的通信
    参数
    - int[2] 这个数组是一个传出参数
    pipe[0] 对应的是管道的读端
    pipe[1] 对应的是管道的写端
    返回值
    成功返回0
    失败返回-1并设置errno
    注意:匿名管道只能用于具有关系的进程间的通信
    int pipe(int pipefd[2]);
    管道默认是阻塞的,如果管道中没有数据,read阻塞,通过管道满了,write阻塞
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <unistd.h>

    int main() {
    // 创建管道,在fork()之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1) {
    perror("pipe");
    exit(0);
    }
    // 创建子进程kenqing
    pid_t pid = fork();
    if (pid > 0) {
    // 父进程
    // 从管道的读取端读取数据
    char buf[1024] = {0};
    int len = read(pipefd[0], buf, sizeof(buf));
    printf("parent recv : %s , pid : %d\n", buf, getpid());
    } else if (pid == 0) {
    // 子进程
    sleep(5);
    char* str = "hello , i am child";
    write(pipefd[1], str, strlen(str));
    }
    return 0;
    }
  • 管道两端同时发送和接受数据,注意顺序需要相反(不能都先接收或者都先发送)

    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
    /*
    #include <unistd.h>
    int pipe(int pipefd[2]);
    功能:创建一个匿名管道,用来进程间通信。
    参数:int pipefd[2] 这个数组是一个传出参数。
    pipefd[0] 对应的是管道的读端
    pipefd[1] 对应的是管道的写端
    返回值:
    成功 0
    失败 -1

    管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞

    注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)
    */

    // 子进程发送数据给父进程,父进程读取到数据输出
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/types.h>
    #include <unistd.h>

    int main() {
    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1) {
    perror("pipe");
    exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0) {
    // 父进程
    printf("i am parent process, pid : %d\n", getpid());

    // 关闭写端
    close(pipefd[1]);

    // 从管道的读取端读取数据
    char buf[1024] = {0};
    while (1) {
    int len = read(pipefd[0], buf, sizeof(buf));
    printf("parent recv : %s, pid : %d\n", buf, getpid());

    // 向管道中写入数据
    char* str = "hello,i am parent";
    write(pipefd[1], str, strlen(str));
    sleep(1);
    }

    } else if (pid == 0) {
    // 子进程
    printf("i am child process, pid : %d\n", getpid());
    // 关闭读端
    close(pipefd[0]);
    char buf[1024] = {0};
    while (1) {
    // 向管道中写入数据
    char* str = "hello,i am child";
    write(pipefd[1], str, strlen(str));
    sleep(1);

    int len = read(pipefd[0], buf, sizeof(buf));
    printf("child recv : %s, pid : %d\n", buf, getpid());
    bzero(buf, 1024);
    }
    }
    return 0;
    }

    • 这段代码有什么缺陷?

      • 如果没有sleep()函数,父子进程可能会一直执行,另一个进程抢不到时间片导致父进程读取自己写的数据或者子进程读取自己写的数据

        image-20230829191742730

        • 如图所示,会发生如上的错误
      • 所以管道一般不会用于两个进程互相发送数据

  • 查看管道的大小fpathconf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <unistd.h>
    #include <sys/types.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    int main() {

    int pipefd[2];

    int ret = pipe(pipefd);

    // 获取管道的大小
    long size = fpathconf(pipefd[0], _PC_PIPE_BUF);

    printf("pipe size : %ld\n", size);

    return 0;
    }
匿名管道通信案例
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
/*
实现ps aux | grep xxx
子进程: ps aus 子进程结束后,将数据发送给父进程
父进程: 获取数据 过滤
pipe()
execlp()
子进程将标准输出 stdout_fileno 重定向到管道的写段 dup2
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>

int main() {
// 创建一个管道
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1) {
perror("pipe");
return -1;
}
// 创建子进程
pid_t pid = fork();
if (pid > 0) {
// 关闭写端
close(pipefd[1]);
char buf[1024] = {0};

// 从管道中读取数据
int len = 1;
while (len = read(pipefd[0], buf, sizeof(buf) - 1)) {
// 过滤数据输出
printf("%s", buf);
memset(buf, 0, sizeof(buf));
}
wait(NULL);

} else if (pid == 0) {
// 关闭读端
close(pipefd[0]);
// 文件描述符重定向 stdout_fileno -> fd[1]
dup2(pipefd[1], STDOUT_FILENO);
// 执行ps aux
execlp("ps", "ps", "aux", NULL);
perror("execlp");
exit(0);
} else {
perror("fork()");
return -1;
}

return 0;
}
管道的读写特点和管道设置为非阻塞
  • 管道的读写特点

    • 注意一下特殊情况(默认阻塞I/O)
      • 1.如果所有指向管道写端的文件描述符都关闭了,写端的引用计数为0,有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0。就像读到文件末尾一样
      • 2.如果有指向管道写端的文件描述符没有关闭,写端的引用计数大于0,而持有管道写端的进程没有往管道的写端写数据,这个时候有进程从管道中读取数据,那么管道中剩余数据被读取后,再次read会阻塞, 直到管道中有数据可以读了才会读取数据并
      • 3.如果所有指向管道读端的文件描述符都关闭了,读端的引用计数为0,有进程从管道的写端写数据,那么该进程会收到一个信号SIGPIPE,通常会导致进程的异常终止
      • 4..如果有指向管道读端的文件描述符没有关闭,读端的引用计数大于0,而持有管道读端的进程没有从管道中读数据,这个时候有进程从管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置之后才能再次写入数据并返回
  • 总结

    • image-20230829200444990
  • 设置管道非阻塞

    • 使用fcntl()函数
    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
    /*
    设置管道非阻塞
    int flags = fcntl(pipefd[0],GETFL); //获取原来的flag
    flags |= O_NONBLOCK
    fcntl(pipefd[0] , F_SETFL , flags);
    */

    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/types.h>
    #include <unistd.h>

    int main() {
    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1) {
    perror("pipe");
    exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0) {
    // 父进程
    printf("i am parent process, pid : %d\n", getpid());

    // 关闭写端
    close(pipefd[1]);

    // 从管道的读取端读取数据
    char buf[1024] = {0};
    // 设置读端为非阻塞
    int flags = fcntl(pipefd[0], F_GETFL);
    flags |= O_NONBLOCK;
    fcntl(pipefd[0], F_SETFL, flags);
    while (1) {
    int len = read(pipefd[0], buf, sizeof(buf));
    printf("len: %d\n", len);
    printf("parent recv : %s, pid : %d\n", buf, getpid());
    sleep(2);
    }

    } else if (pid == 0) {
    // 子进程
    printf("i am child process, pid : %d\n", getpid());
    // 关闭读端
    close(pipefd[0]);
    char buf[1024] = {0};
    while (1) {
    // 向管道中写入数据
    char* str = "hello,i am child";
    write(pipefd[1], str, strlen(str));
    memset(buf,0,sizeof(buf));
    sleep(10);
    }
    }
    return 0;
    }

有名管道

有名管道介绍及使用
  • 匿名管道只能用于亲缘关系的进程间通信。有名管道是克服这个缺点而提出的

  • 有名管道FIFO不同于匿名管道之处在于它提供了一个路径名与之关联

    • FIFO的文件形式存在于文件系统中,打开方式与打开一个普通文件是一样的
  • image-20230829203957644

  • image-20230829204131910

    • 不同之处:
      • FIFO有文件实体,但是内容却存放在内存中
      • FIFO进程退出后文件将继续保存在文件系统中
  • 有名管道的使用

    • image-20230829204246760
  • 有名管道的注意事项

    • 一个只读管道打开时会阻塞,直到一个只写进程打开管道
      • 反之亦然
  • 读管道:

    • 管道中有数据,read返回实际读到的字节数
    • 管道无数据:
      • 管道写端被全部关闭,,read返回0(相当于读到文件末尾)
      • 管道写端没有被全部关闭,read阻塞
  • 写管道

    • 管道读端被全部关闭,进程异常终止(收到SIGPIPE信号)
    • 管道读端没有全部关闭
      • 管道已经满了,write会阻塞
      • 管道没有满,write将数据写入并返回实际写入的数据量大小
有名管道实现简单的聊天功能
  • chatA

    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
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <unistd.h>

    int main() {
    // 1.判断有名管道是否存在
    int ret = access("fifo1", F_OK);
    if (ret == -1) {
    printf("管道不存在,创建对应的有名管道fifo1\n");
    ret = mkfifo("fifo1", 0664);
    if (ret == -1) {
    perror("mkfifo");
    exit(0);
    }
    }
    ret = access("fifo2", F_OK);
    if (ret == -1) {
    printf("管道不存在,创建对应的有名管道fifo2\n");
    ret = mkfifo("fifo2", 0664);
    if (ret == -1) {
    perror("mkfifo");
    exit(0);
    }
    }
    // 2.以只读的方式打开管道1
    int fdr = open("fifo1", O_RDONLY);
    if (fdr == -1) {
    perror("open");
    exit(0);
    }
    printf("打开fifo1成功,等待读取数据...\n");
    // 3.以只写的方式打开管道2
    int fdw = open("fifo2", O_WRONLY);
    if (fdw == -1) {
    perror("open");
    exit(0);
    }
    printf("打开fifo2成功,等待写入数据...\n");
    // 4.循环读写数据
    char buf[128];
    while (1) {
    // 5.读管道数据
    memset(buf, 0, 128);
    ret = read(fdr, buf, 128);
    if (ret <= 0) {
    perror("read");
    break;
    }
    printf("buf: %s\n", buf);

    // 获取数据
    memset(buf, 0, 128);
    fgets(buf, 128, stdin);
    // 写数据
    ret = write(fdw, buf, strlen(buf));
    if (ret == -1) {
    perror("write");
    exit(0);
    }
    }
    // 6.关闭文件描述符
    close(fdr);
    close(fdw);

    return 0;
    }
  • chatB

    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
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <unistd.h>

    int main() {
    // 1.判断有名管道是否存在
    int ret = access("fifo1", F_OK);
    if (ret == -1) {
    printf("管道不存在,创建对应的有名管道fifo1\n");
    ret = mkfifo("fifo1", 0664);
    if (ret == -1) {
    perror("mkfifo");
    exit(0);
    }
    }
    ret = access("fifo2", F_OK);
    if (ret == -1) {
    printf("管道不存在,创建对应的有名管道fifo2\n");
    ret = mkfifo("fifo2", 0664);
    if (ret == -1) {
    perror("mkfifo");
    exit(0);
    }
    }
    // 2.以只写的方式打开管道1
    int fdw = open("fifo1", O_WRONLY);
    if (fdw == -1) {
    perror("open");
    exit(0);
    }
    printf("打开fifo1成功,等待写入数据...\n");
    // 3.以只读的方式打开管道2
    int fdr = open("fifo2", O_RDONLY);
    if (fdr == -1) {
    perror("open");
    exit(0);
    }
    printf("打开fifo2成功,等待读取数据...\n");
    // 4.循环写读数据
    char buf[128];
    while (1) {
    // 获取数据
    memset(buf, 0, 128);
    fgets(buf, 128, stdin);
    // 写数据
    ret = write(fdw, buf, strlen(buf));
    if (ret == -1) {
    perror("write");
    exit(0);
    }
    // 5.读管道数据
    memset(buf, 0, 128);
    ret = read(fdr, buf, 128);
    if (ret <= 0) {
    perror("read");
    break;
    }
    printf("buf: %s\n", buf);
    }
    // 6.关闭文件描述符
    close(fdr);
    close(fdw);

    return 0;
    }

内存映射 memory-mapped I/O

  • 将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件
  • 内存映射的相关系统调用
    • image-20230830192010155
      • mmap内存映射
      • mumap内存解映射
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
/*
#include <sys/mman.h>
功能: 映射一个文件的数据到内存中
参数
- void * addr:NULL,由内核决定
- length: 要映射的数据的长度,这个值不能为0,建议使用文件的长度
获取文件长度 stat 、 lseek
- prot: 对申请的内存映射区的操作权限
PROT_EXEC Pages may be executed.可执行权限

PROT_READ Pages may be read.读权限

PROT_WRITE Pages may be written.写权限

PROT_NONE Pages may not be accessed.没有权限
要操作映射区内存,必须要有读的权限
- flags:
- MAP_SHARED: 映射区的数据会自动和磁盘文件进行同步,进程间通信必须要设置这个权限
- MAP_PRIVATE: 不同步,映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件
- fd: 需要映射的那个文件的文件描述符
-通过open得到,open的是一个磁盘文件
-注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突
PROT: PROT_READ->open:只读/读写
PROT: PROT_READ | PROT_WRITE open:只能是读写
- offset: 偏移量,一般不用。碧玺指定的是4k的整数倍,0表示不偏移,从文件开头
返回:返回创建的内存的首地址
失败返回MAP_FAILED.(void*) -1
void *mmap(void addr[.length], size_t length, int prot, int flags,int fd, off_t offset);
int munmap(void addr[.length], size_t length);
功能:释放内存映射
参数
- addr: 要释放的内存的首地址
- length 要释放的大小,与mmap中的length一样

*/

/*
使用内存映射实现进程间通信:
1.有关系的进程(父子进程)
-在没有子进程的时候创建内存映射区
-有了内存映射区之后再创建子进程
-父子进程共享创建的内存映射区
2.没有关系的进程
-准备大小不是0的磁盘文件
-进程1,通过磁盘文件创建内存映射区
-得到一个操作这块内存的指针
-进程2,通过磁盘文件创建内存映射区
-得到一个操作这块内存的指针
-使用内存映射区进行通信
注意:内存映射区是非阻塞的
*/

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>

//有关系的进程通信
int main() {
// 1.打开这个文件
int fd = open("test.txt", O_RDWR);
if (fd == -1) {
perror("open");
exit(0);
}
// 1.获取文件大小
int size = lseek(fd, 0, SEEK_END);

// 2.创建内存映射区
void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
exit(0);
}

// 3.创建子进程
pid_t pid = fork();

if (pid > 0) {
wait(NULL);
char buf[64];
strcpy(buf, (char*)ptr);
printf("read data : %s\n", buf);
} else if (pid == 0) {
strcpy((char*)ptr, "nihao a , son!!!");
}

// 关闭内存映射区
munmap(ptr, size);

return 0;
}
思考问题
  • 1.如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?

    void* ptr = mmap(…);

    可以对ptr进行操作但是不建议,导致munmap(…)// 错误,要保存地址

  • 2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?

    错误,返回MAP_FAILED
    open()函数中的权限建议和prot参数的权限保持一致。

    prot的权限≤open的权限

  • 3.如果文件偏移量为1000会怎样?

    偏移量要求是4k的整数倍,会返回错误MAP_FAILED

  • 4.mmap什么情况下会调用失败?

    • 第二个参数length = 0
    • 第三个参数prot权限没有指定读权限
    • 第三个参数prot权限大于open的权限
  • 5.可以open的时候O_CREAT一个新文件来创建映射区吗?、

    • 可以的,但是创建的文件的大小如果为0的话,肯定不行
      • 可以对新的文件进行扩展
        • lseek()
        • truncate()
  • 6.mmap后关闭文件描述符,对mmap映射有没有影响?

    int fd = open(“XXX”);
    mmap(,,,,fd,0);
    close(fd);

    映射区还在,创建映射区的fd被关闭没有任何影响

  • 7.对ptr越界操作会怎样?

    void * ptr = mmap(NULL, 100,,,,,);
    4K
    越界操作操作的是非法的内存 -> 段错误

文件复制

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
// 使用内存映射实现文件复制

/*
思路:
1.对原始的文件进行内存映射
2.创建一个新文件,拓展新文件
3.把新文件数据映射到内存中
4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存中
*/

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
// 1.对原始的文件进行内存映射
int fd = open("english.txt", O_RDWR);
if (fd == -1) {
perror("open");
exit(0);
}

// 获取原始文件的大小
int len = lseek(fd, 0, SEEK_END);

// 2.创建一个新文件,拓展新文件
int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);
if (fd1 == -1) {
perror("open");
exit(0);
}

truncate("cpy.txt", len);
write(fd1, " ", 1);

// 3.把新文件数据映射到内存中
void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
void * ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
exit(0);
}

if (ptr1 == MAP_FAILED) {
perror("mmap");
exit(0);
}

// 4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存中
memcpy(ptr1, ptr, len);

// 5.释放资源 遵循FILO原则
munmap(ptr1, len);
munmap(ptr, len);
close(fd1);
close(fd);

return 0;
}
匿名映射
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
/*
匿名映射:不需要文件实体进程一个内存映射
只能用于父子进程间的映射
*/

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
// 1.创建匿名内存映射区
int len = 4096;
void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
exit(0);
}
// 父子进程间通信
pid_t pid = fork();
if (pid > 0) {
strcpy((char*)ptr, "hello world");
wait(NULL);
} else if (pid == 0) {
sleep(1);
printf("%s \n", (char*)ptr);
}
int ret = munmap(ptr, len);
if(ret == -1){
perror("munmap");
exit(0);
}


return 0;
}

信号

  • 是事件发生时对进程的通知机制,也称之为软件中断。异步通信

    • image-20230830221142315
    • image-20230830221548859
  • 使用信号的目的

    • 让进程知道已经发生了一个特定事情
    • 强迫进程执行它自己代码中的信号处理程序
  • 信号的特点

    • 简单
    • 不能携带大量信息
    • 满足某个特定条件才发送
    • 优先级比较高

    image-20230830221810161

  • kill -l

    • image-20230830221946836

      • 1-31号信号是linux操作系统的常规信号
      • 34-64是预定义好的信号,目前还没有使用
    • image-20230830222044955

    • image-20230830222431955

    • image-20230830222630578

      • 特别注意标红信号

      image-20230830222702645

  • 信号的五种默认处理动作

    • image-20230830223215080
      • 当进程收到信号后,必须做以上五件事情之一
      • core保存进程异常退出的错误信息。
        • ulimit -a 查看
        • ulimit -c 大小 设置产生错误信息文件core的大小
kill、raise、abort函数
  • image-20230831090449637

  • 代码示例

    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
    /*
    #include <signal.h>
    功能:给某个进程pid发送某个信号sig 。任何进程发送任何信号
    参数:
    - pid: 需要发送给的进程id
    pid > 0: 将信号发送给指定的进程
    pid = 0: 将信号发送给当前进程组
    pid = -1: 将信号发送给每一个有权限接受这个信号的进程
    pid < -1: 这个pid是某个进程组的PID取反,0表示不发送任何信号
    - sig: 需要发送的信号的编号或者是宏值(两者等价,建议使用宏值)
    int kill(pid_t pid, int sig);

    #include <signal.h>
    功能:给当前进程发送信号,等同于kill(getpid(),sig);
    参数
    -sig: 表示要发送的信号
    返回值
    成功返回0
    失败返回非0
    int raise(int sig);

    #include <stdlib.h>
    功能:发送SIGABRT信号给当前进程,杀死当前进程 等同于kill(getpid(),SIGABRT);
    void abort(void);

    */
    #include <signal.h>
    #include <stdio.h>
    #include <string.h>
    #include <sys/types.h>
    #include <unistd.h>

    int main() {
    pid_t pid = fork();
    if (pid == 0) {
    int i = 0;
    for (i = 0; i < 5; i++) {
    printf("child process%d\n", i);
    sleep(1);
    }
    } else if (pid > 0) {
    printf("parent process\n");
    sleep(2);
    printf("parent kills child process\n");
    kill(pid, SIGINT);
    }
    return 0;
    }
alarm函数
  • image-20230831093120547

  • alarm使用方式

    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
    /*
    #include <unistd.h>
    功能:设置定时器
    参数:
    - seconds:函数调用开始倒计时,当倒计时为0时,函数给当前进程发送一个信号:SIGALARM,进程终止
    单位:秒 若参数为0,定时器无效
    取消定时器:通过alarm(0)
    返回值:
    - 之前有定时器,返回之前定时器剩余时间
    - 之前没有定时器,返回0
    -SIGALARML: 默认终止当前的进程,每一个进程都有且仅有唯一的一个定时器
    重复调用会刷新定时器,返回的是之前定时器剩余的时间
    unsigned int alarm(unsigned int seconds);
    该函数是不阻塞的
    */

    #include <stdio.h>
    #include <unistd.h>

    int main() {
    int seconds = alarm(5);
    printf("第一次调用 seconds: %d\n", seconds);
    sleep(2);
    seconds = alarm(5);
    printf("休眠两秒后第二次调用 seconds: %d\n", seconds);
    while (1) {
    ;
    }
    return 0;
    }
  • 案列:查看电脑一秒能往文件中写多少个数

1
2
3
4
5
6
7
8
9
10
11
// 1秒电脑能数多少个数
#include <stdio.h>
#include <unistd.h>
int main() {
alarm(1);
int i = 0;
while (1) {
printf("%d\n", i++);
}
return 0;
}
* 实际时间 = 内核时间 + 用户时间(代码执行) + 消耗时间(I/O)、
* 进行文件IO操作非常浪费时间
  • 定时器与进程的状态无关(自然定时法)

    • 无论进程处于什么状态,alarm()都会计时
setitimer定时器函数
  • 使用方式
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
/*
#include <sys/time.h>
功能: 设置定时器(闹钟),可以替代alarm,精度微秒us,实现周期性定时
参数:
- which: 定时器以什么时间计时
ITIMER_REAL: 真实时间,时间到达,发送SIGALARM信号 常用
ITIMER_VIRTUAL: 用户时间,时间到达,发送SIGVTALRM信号
ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算
- new_value: 设置定时器时间属性,
struct itimerval { //定时器结构体
struct timeval it_interval; //每个阶段的时间,间隔时间
struct timeval it_value; //延迟多长时间执行定时器
};

struct timeval { // 时间结构体
time_t tv_sec; //秒
suseconds_t tv_usec; //微秒
};
- old_value: 记录上一次的定时的时间参数 ,一般不使用,传递NULL
返回值:
成功为0
失败-1并设置错误号


int setitimer(int which, const struct itimerval *restrict new_value,
struct itimerval *_Nullable restrict old_value);
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <unistd.h>

int main() {
struct itimerval new_value;
// 设置值 过3秒以后,每隔两秒定时一次
new_value.it_value.tv_sec = 3; // 设置延迟时间
new_value.it_value.tv_usec = 0;
new_value.it_interval.tv_sec = 2; // 设置间隔时间
new_value.it_interval.tv_usec = 0;

int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了\n");
int i = 1;
while (1) {
printf("%d\n", i++);
sleep(1);
}

if (-1 == ret) {
perror("setitimer");
exit(0);
}
getchar();

return 0;
}
signal信号
  • setitimer不能看到周期性的循环,发的是结束进程的信号
  • signal可以捕捉信号,解决了setitimer不能捕捉信号的缺点
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
/*
#include <signal.h>
typedef void (*sighandler_t)(int);
功能:设置某个信号的捕捉行为
参数:
- signum: 要捕捉的信号
- handler: 捕捉到信号后如何处理
SIG_IGN: 忽略信号
SIG_DFL: 默认信号,使用信号默认行为
回调函数: 这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号
-需要程序员实现并且提前准备好,函数类型根据实际需求,看函数指针定义
-不需要程序员调用,而是当信号产生时由内核调用
-函数指针是实现回调的手段。函数实现之后,将函数名放到函数指针的位置就可以了
返回值:
成功返回上一次注册的信号处理函数的地址,第一次调用返回NULL
失败返回宏SIG_ERR,设置错误号
sighandler_t signal(int signum, sighandler_t handler);

注意: The signals SIGKILL and SIGSTOP cannot be caught or ignored.
*/

#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <unistd.h>

void myalarm(int num){
printf("捕捉到了信号的编号是%d\n",num);
printf("这是回调函数\n");
}


int main() {
//注册信号捕捉
signal(SIGALRM,myalarm);
//typedef void (*sighandler_t)(int); 函数指针 sighandler_t是函数指针的名称


struct itimerval new_value;
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL);
printf("定时器开始了\n");
int i = 1;
while (1) {
printf("%d\n", i++);
sleep(1);
}
if (-1 == ret) {
perror("setitimer");
exit(0);
}

getchar();
return 0;
}
自定义信号集相关函数
  • 信号集

    • 信号组成的集合 sigset_t 整型
    • image-20230901125922741
  • PCB中两个重要的信号集

    • 阻塞信号集未决信号集
      • 这两个信号集都是内核使用位图机制来实现的(二进制位)
      • 不能直接对这两个信号集进行位操作。需要自定义另一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改
    • 未决:是一种状态机。指的是信号的产生到信号被处理签的这一段事件
    • 阻塞:是一个开关动作,指的是阻止信号被处理,而不是阻止信号的产生
      • 阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下的信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作
  • 工作原理

    • image-20230901131201436

    1.用户通过键盘 Ctrl + C,产生2号信号SIGINT(信号被创建)

    2.此时信号产生但是没有被处理(未决)

    • 在内核中将所有的没有被处理的信号存储在一个集合中,这个集合被称为未决信号集
    • SIGINT信号状态被存储在第二个标志位
    • 这个标志位的值为0,说明信号不处于未决状态
    • 这个标志位的值为1,说明信号处于未决状态
    • 这个未决状态的信号需要被处理,处理之前需要和另一个信号集(阻塞信号集)进行比较。
    • 阻塞信号集中对应信号的标志位的值若为1,表示阻塞,信号一直处于未决状态直到阻塞接触;若值为0,处理该信号,处理完成之后未决信号集中对应的标志位值置为0
    • 若想要阻塞某些信号,需要调用系统API
  • 跟信号集相关的函数

    • image-20230901132138872
  • 这里出现的函数只能对自定义的信号集进行操作

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
/*

#include <signal.h>
以下信号集相关函数都是对自定义的信号集进行操作

int sigemptyset(sigset_t *set);
功能:清空信号集中的数据,将信号集中的所有的标志位置为0
参数:
- set:传出参数,是我们需要操作的信号集
返回值:
成功返回0
失败返回-1
int sigfillset(sigset_t *set);
功能:将信号集中所有的标志位置为1
参数:
- set:传出参数,是我们需要操作的信号集
返回值:
成功返回0
失败返回-1
int sigaddset(sigset_t *set, int signum);
功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
参数:
- set:传出参数,是我们需要操作的信号集
- signum: 需要设置阻塞的信号
返回值:
成功返回0
失败返回-1
int sigdelset(sigset_t *set, int signum);
功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
参数:
- set:传出参数,是我们需要操作的信号集
- signum: 需要设置不阻塞的信号
返回值:
成功返回0
失败返回-1
int sigismember(const sigset_t *set, int signum);
功能:判断某个信号是否阻塞
参数:
- set:是我们需要操作的信号集
- signum: 需要判断的信号
返回值:
1:signum被阻塞
0:signum不阻塞
-1:失败
*/

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/signal.h>
#include<fcntl.h>
#include<sys/types.h>

int main() {
// 创建一个信号集
sigset_t set;
//清空信号集
sigemptyset(&set);

//判断SIGINT是否在信号集set里
int ret = sigismember(&set, SIGINT);
if(ret == 0){
printf("SIGINT不阻塞\n");
}else if(ret == 1){
printf("SIGINT阻塞\n");
}
//添加几个信号到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
//判断SIGQUIT是否在信号集中
ret = sigismember(&set, SIGQUIT);
if(ret == 0){
printf("SIGQUIT不阻塞\n");
}else if(ret == 1){
printf("SIGQUIT阻塞\n");
}

//从信号集中删除信号
sigdelset(&set, SIGQUIT);
ret = sigismember(&set, SIGQUIT);
if(ret == 0){
printf("SIGQUIT不阻塞\n");
}else if(ret == 1){
printf("SIGQUIT阻塞\n");
}

return 0;
}
sigprocmask函数
  • 内核的信号集进行操作
  • 内核信号集不能直接修改,需要先自定义信号集,用自定义的信号集去设置内核中的信号集,这里就需要使用sigprocmask函数和上图中的最后一个函数
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
/*
#include <signal.h>
功能: 将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
参数
- how: 如何对内核中阻塞的信号集进行处理
SIG_BLOCK:将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
假设内核中,默认的阻塞信号集是mask: mask | set
SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
mask &= ~set
SIG_SETMASK: 覆盖内核中原来的值
- set: 已经初始化好的用户自定义的信号集
-oldset: 保存设置之前的内核中的阻塞信号集的状态,一般不使用,NULL
返回值
成功返回 0
失败返回 -1 设置错误号EFAULT、EINVAL
int sigprocmask(int how, const sigset_t *_Nullable restrict set,
sigset_t *_Nullable restrict oldset);

功能: 获取内核中的未决信号集
参数:
-set: 传出参数,保存的是未决中的传出信号集中的信息
int sigpending(sigset_t *set);
*/

#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// 目的:将所有的常规信号(1-31)的未决状态打印到屏幕
int main() {
sigset_t set;
sigemptyset(&set);
// 设置2和3号信号阻塞
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 修改内核中的阻塞信号集
sigprocmask(SIG_BLOCK, &set, NULL);
int n = 0;
while (1) {
// 获取当前未决信号集的数据
sigset_t pendingset;
sigemptyset(&pendingset);
sigpending(&pendingset);

// 遍历
for (int i = 1; i <= 32; i++) {
if (sigismember(&pendingset, i) == 1) {
printf("1");
} else if (sigismember(&pendingset, i) == 0) {
printf("0");
} else {
perror("sigismenber");
exit(0);
}
}
printf("\n");
sleep(1);
n++;
if(n == 10){
//解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
}

}

return 0;
}
sigaction信号捕捉函数
  • 建议使用sigaction,因为signal是美国定义的,不同标准下结果可能不一样

  • image-20230901160954448

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
/*
#include <signal.h>
功能:检查或者改变信号的处理。信号捕捉
参数
- signum: 需要捕捉的信号的编号或者宏值(信号的名称)
-act:捕捉到信号之后相应的处理动作
-oldact:一般为NULL,上一次信号捕捉相关的设置
返回值
成功返回0,失败返回-1
int sigaction(int signum, struct sigaction * act,
struct sigaction * oldact);

struct sigaction {
void (*sa_handler)(int); //函数指针,只想的函数就是信号捕捉到之后的处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); //不常用,
sigset_t sa_mask; //临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号
int sa_flags; // 使用哪一个信号对捕捉到的信号进行处理 0表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
void (*sa_restorer)(void); //被废弃掉,NULL
};

*/

#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <unistd.h>

void myalarm(int num) {
printf("捕捉到了信号的编号是%d\n", num);
printf("这是回调函数\n");
}

int main() {
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myalarm;
// 清空临时阻塞信号集
sigemptyset(&act.sa_mask);

// 注册信号捕捉
sigaction(SIGALRM, &act, NULL);
// typedef void (*sighandler_t)(int); 函数指针 sighandler_t是函数指针的名称

struct itimerval new_value;
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL);
printf("定时器开始了\n");
int i = 1;
while (1) {
printf("%d\n", i++);
sleep(1);
}
if (-1 == ret) {
perror("setitimer");
exit(0);
}

getchar();
return 0;
}
sigchild信号
  • 产生的三个条件

    • 子进程终止
    • 子进程接收到SIGSTOP信号停止,注意这里不是进程结束,而是处于暂停态
    • 子进程处于停止态,接受到SIGCONT后唤醒时
  • 发送sigchild是由内核发送给父进程的信号,父进程默认忽略该信号

    • 可以解决僵尸进程的问题
    • 避免父进程使用wait()盲目等待
  • 案例

    • 注意:提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
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
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

void myFun(int num) {
printf("捕捉到的信号: %d\n", num);
// 回收子进程的资源
// while (1) {
// wait(NULL);
// }

while (1) {
int ret = waitpid(-1, NULL, WNOHANG);
if (ret > 0) {
printf("child die, pid = %d\n", getpid());
} else if (ret == 0) {
// 说明还有子进程活着
break;
} else {
// 没有子进程了
break;
}
}
}

int main() {
// 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);

// 创建一些子进程
pid_t pid;
for (int i = 0; i < 20; i++) {
pid = fork();
if (pid == 0) {
break;
}
}
if (pid > 0) {
// 父进程

// 捕捉子进程死亡时发送的sigchild信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myFun;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);

// 注册完信号捕捉以后,就解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);

while (1) {
printf("parent process pid: %d\n", getpid());
sleep(2);
}
} else if (pid == 0) {
printf("child process oid: %d\n", getpid());
}
return 0;
}

共享内存

  • 效率最高的进程间通信方式

  • 允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分。因此这种IPC机制无需内核介入(并不是完全不需要)。

  • 一个进程将数据复制进共享内存中,并且这部分数据对其他所有共享同一个段的进程可用

    image-20230902142550015

  • 共享内存使用步骤

    • image-20230902142831824
    • 1.调用shmget(),创建一个新的共享内存或者取得一个既有共享内存段的标识符。这个调用返回后续调用中需要用到的共享内存标识符
    • 2.使用shmat()来附上共享内存段,使该段称为调用进程的虚拟内存的一部分。
    • 3.shmat()返回值addr指向进程的虚拟地址空间中该共享内存段的起点
    • 4.调用shmdt()来分离共享内存段。调用之后,进程就无法再引用这块共享内存了。
    • 5.调用shmctl()来删除共享内存段。只需要一个进程执行这一步
  • 共享内存操作函数

    • image-20230902144012388
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
共享内存相关的函数
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
- 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。
新创建的内存段中的数据都会被初始化为0
- 参数:
- key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。
一般使用16进制表示,非0
- size: 共享内存的大小
- shmflg: 属性
- 访问权限
- 附加属性:创建/判断共享内存是不是存在
- 创建:IPC_CREAT
- 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
IPC_CREAT | IPC_EXCL | 0664
- 返回值:
失败:-1 并设置错误号
成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。


void *shmat(int shmid, const void *shmaddr, int shmflg);
- 功能:和当前的进程进行关联
- 参数:
- shmid : 共享内存的标识(ID),由shmget返回值获取
- shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定
- shmflg : 对共享内存的操作
- 读 : SHM_RDONLY, 必须要有读权限
- 读写: 0
- 返回值:
成功:返回共享内存的首(起始)地址。 失败(void *) -1


int shmdt(const void *shmaddr);
- 功能:解除当前进程和共享内存的关联
- 参数:
shmaddr:共享内存的首地址
- 返回值:成功 0, 失败 -1

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响。
- 参数:
- shmid: 共享内存的ID
- cmd : 要做的操作
- IPC_STAT : 获取共享内存的当前的状态
- IPC_SET : 设置共享内存的状态
- IPC_RMID: 标记共享内存被销毁
- buf:需要设置或者获取的共享内存的属性信息
- IPC_STAT : buf存储数据
- IPC_SET : buf中需要初始化数据,设置到内核中
- IPC_RMID : 没有用,NULL

key_t ftok(const char *pathname, int proj_id);
- 功能:根据指定的路径名,和int值,生成一个共享内存的key
- 参数:
- pathname:指定一个存在的路径
/home/nowcoder/Linux/a.txt
/
- proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
范围 : 0-255 一般指定一个字符 'a'


问题1:操作系统如何知道一块共享内存被多少个进程关联?
- 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
- shm_nattach 记录了关联的进程个数

问题2:可不可以对共享内存进行多次删除 shmctl
- 可以
- 因为shmctl 标记删除共享内存,不是直接删除
- 什么时候真正删除呢?
当和共享内存关联的进程数为0的时候,就真正被删除
- 当共享内存的key为0的时候,表示共享内存被标记删除了
如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。

共享内存和内存映射的区别
1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
2.共享内存效果更高
3.内存
所有的进程操作的是同一块共享内存。
内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
4.数据安全
- 进程突然退出
共享内存还存在
内存映射区消失
- 运行进程的电脑死机,宕机了
数据存在在共享内存中,没有了
内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。

5.生命周期
- 内存映射区:进程退出,内存映射区销毁
- 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
如果一个进程退出,会自动和共享内存进行取消关联。
  • 共享内存操作命令

    image-20230902154949940

  • 共享内存使用示例

  • 写端

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
#include <memory.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main() {
// 1.创建一个共享内存
int shmid = shmget(100, 4096, IPC_CREAT | 0664);
printf("shmid: %d\n", shmid);

// 2.和当前进程进行关联
void* ptr = shmat(shmid, NULL, 0);

char* str = "hello world";
// 3.写数据
memcpy(ptr, str, strlen(str) + 1);

printf("按任意键继续\n");
getchar();

// 4.解除关联
shmdt(ptr);

// 5.删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
  • 读端
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
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main() {
// 1.获取一个共享内存
int shmid = shmget(100, 0, IPC_CREAT);
printf("shmid: %d\n", shmid);

// 2.和当前进程进行关联
void* ptr = shmat(shmid, NULL, 0);

// 3.读数据
printf("%s\n", (char*)ptr);

printf("按任意键继续\n");
getchar();

// 4.解除关联
shmdt(ptr);

// 5.删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;

}

进程间通信方式及应用场景总结

管道

进程间数据间的简单传输

  • 用于进程间的简单的数据传输
    • 若是父子进程间通信,用无名管道就行。pipe()创建管道
    • 若是不同进程间的简单通信,用有名管道,mkfifo()创建管道

共享内存

当不同进程间需要共享数据时,需要用到共享内存

  • 内存映射:映射一个文件的数据到内存中

    • mmap创建一块共享内存,返回指向这块内存的指针,多个进程共享这一块内存
  • 内存共享:效率最高的一种通信方式

    • 一个进程将数据复制进共享内存中,并且这部分数据对其他所有共享同一个段的进程可用
1
2
3
4
5
- 1.调用shmget(),创建一个新的共享内存或者取得一个既有共享内存段的标识符。这个调用返回后续调用中需要用到的共享内存标识符
- 2.使用shmat()来附上共享内存段,使该段称为调用进程的虚拟内存的一部分。
- 3.shmat()返回值addr指向进程的虚拟地址空间中该共享内存段的起点
- 4.调用shmdt()来分离共享内存段。调用之后,进程就无法再引用这块共享内存了。
- 5.调用shmctl()来删除共享内存段。只需要一个进程执行这一步

信号量

是进程间同步的一种机制。用于管理多个进程对于共享资源的访问

消息队列

使用于进程间大量传递数据的情况

套接字

用于不同计算机中的进程通信


进程组、会话

  • 终端:
    • 在UNIX系统中,用户通过终端登录系统后得到一个shell进程,这个终端成为shell进程的控制终端
    • 进程中,控制终端是保存在PCB中的信息,而fork()会复制PCB中的信息,因此由shell进程启动的其他进程的控制终端也是这个终端
    • 标准输入和标准错误输出默认情况下都指向控制终端
  • image-20230902191058750
  • 进程组
    • 进程组是很多进程的集合,会话是一组相关进程组的集合
    • 进程组和会话是为了支持shell作用控制而定义的抽象概念,用户通过shell能够交互式地在前台后后台运行命令
    • 进程组组成
      • 一个或多个共享同一进程组标识符PGID的进程组成。
      • 一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程ID是该进程组的PGID。新进程会继承其父进程所属的进程组ID
    • image-20230902192641858
  • 会话
    • image-20230902192807449
  • 进程组、会话、控制终端之间的关系
    • image-20230902193608403

进程组、会话操作函数

*

  • image-20230902193743447

守护进程

  • 是linux中的后台服务进程。生存周期较长,通常独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件,通常以d结尾

    • 后台进程,周期性的执行某些事情
  • image-20230902193933928

  • 系统启动时候被创建,一直运行直到系统关闭

  • 守护进程的创建步骤

    • image-20230902195356397

    • 退出父进程是避免父进程结束时终端出现提示符

      调用setsid()不会现控制终端,创建新的会话,用子进程是为了避免出现进程组id相同的进程组

  • 守护进程代码示例

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
/*
写一个守护进程,每隔两秒获取系统时间,将这个时间写入到磁盘文件中
*/

#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

void work(int num) {
// 捕捉到信号之后,获取系统时间,写入磁盘
time_t tm = time(NULL);
// 转换格式
struct tm* loc = localtime(&tm);
// char buf[1024];
// sprintf(buf, "%d-%d-%d %d:%d:%d\n", loc->tm_year, loc->tm_mon, loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);
// printf("%s\n", buf);

char* str = asctime(loc);
int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
write(fd, str, strlen(str));
close(fd);
}

int main() {
//1.创建子进程
pid_t pid = fork();
if (pid > 0) {
exit(0);
}
// 2.将子进程提升为会话
setsid();

// 3.设置掩码
umask(022);

// 4.更改工作目录
chdir("/home/yjx");

// 5.关闭,重定向文件描述符
int fd = open("/dev/null/", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);

// 6.业务逻辑

// 捕捉定时信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = work;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
// 创建定时器
struct itimerval val;
val.it_interval.tv_sec = 2;
val.it_interval.tv_usec = 0;
val.it_value.tv_sec = 2;
val.it_value.tv_usec = 0;

setitimer(ITIMER_REAL, &val, NULL);
while (1)
sleep(10); // 死循环

return 0;
}

多线程开发

线程概述

  • image-20230904090526122
    • 一个进程可以有多个线程,同一个程序中的所有线程独立执行相同程序,共享同一份全局内存区域
    • 进程是CPU分配资源的基本单位,线程是操作系统调度执行的最小单位
    • Linux是轻量级的进程LWP。在Linux环境下线程的本质还是进程
    • LWP是线程号
  • 进程和线程的区别
    • image-20230904091130967
    • 进程开销大,信息难以共享
    • 线程方便创建,快速共享信息
  • 进程的创建
    • 拷贝一份新的虚拟地址空间
      • 读时共享,写时复制
  • 线程的创建
    • 共享原来虚拟地址空间。text段和栈空间分成多份给不同线程。其他的段所有线程共 享

image-20230904091608030

  • 线程之间共享和不共享的资源

    image-20230904091734399

    • 即内核中的数据共享
  • NPTL

    • image-20230904091920214

      image-20230904092151610

创建线程

  • 线程操作
    • image-20230904092816687
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
/*
一般情况下,main函数所在的线程称之为主线程,其他创建的线程称为子线程
#include <pthread.h>
功能:创建一个子线程
参数
- thread:传出参数,线程创建成功之后子线程的id
- attr:设置线程的属性,一般使用默认值NULL
- start_routine:函数指针,子线程需要处理的逻辑代码
- arg:给第三个参数使用,传参
返回值
- 成功返回0
- 错误返回错误号,与之前的errno不一样
获取错误号的信息: char* strerror(errno); #include <string.h>
int pthread_create(pthread_t * thread,const pthread_attr_t * attr,
void *(*start_routine)(void *),void * arg);
*/

#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

void* callback(void* arg) {
printf("child thread...\n");
return NULL;
}

int main() {
// 创建一个子进程
pthread_t tid;
int ret = pthread_create(&tid, NULL, callback, NULL);
if (ret != 0) {
char* str = strerror(ret);
printf("error: %s\n", str);
}

for (int i = 0; i < 5; i++) {
printf("%d\n", i);
sleep(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 <pthread.h>
功能:终止一个线程,在哪个线程中调用表示终止哪个线程
参数
- retval:指针,作为一个返回值,可以在pthread_join()中获取到
没有返回值
void pthread_exit(void *retval);

*/
#include <pthread.h>
#include <stdio.h>
#include <string.h>

void* callback(void* arg) {
printf("child thread id: %ld\n", pthread_self());
return NULL;
}

int main() {
// 创建一个子线程
pthread_t tid;
int ret = pthread_create(&tid, NULL, callback, NULL);
if (ret != 0) {
char* str;
str = strerror(ret);
printf("error: %s\n", str);
}
// 主线程
for (int i = 0; i < 5; i++) {
printf("%d\n", i);
}
// 打印id
printf("tid: %ld, parent thread id: %ld\n", tid, pthread_self());
// 让主线程退出,当主线程退出时,不会影响其他正常运行的线程
pthread_exit(NULL);
printf("main thread exit\n"); //不会执行
return 0; //没有执行,进程没有退出
}


连接已终止的进程

  • 该函数的作用是回收子进程资源

    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
    /*
    #include <pthread.h>
    功能: 和一个已经终止的线程进行连接。对线程的资源进行释放,回收资源
    这个函数是阻塞函数,调用一次回收一个子线程
    一般在主线程中使用
    参数:
    - thread:指定回收的线程号
    - retval:接受子线程退出时的返回值
    为什么是二级指针:
    返回值
    成功返回0
    失败返回错误号,非零
    int pthread_join(pthread_t thread, void **retval);
    */
    #include <pthread.h>
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>

    int value = 10; // 定义全局变量
    void* callback(void* arg) {
    printf("child thread id: %ld\n", pthread_self());
    sleep(3);
    // int tmp = 100; //这里是子进程的栈空间,当该线程结束之后是随机值,
    pthread_exit((void*)&value); // 等同于return (void*)&tmp;
    }

    int main() {
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, callback, NULL);

    if (ret != 0) {
    char* str;
    str = strerror(ret);
    printf("error: %s\n", str);
    }

    // 主线程调用pthread_join()回收子线程资源
    int* thread_retval;
    ret = pthread_join(tid, (void**)&thread_retval); // 要回收的子线程没有结束,那么程序阻塞

    if (ret != 0) {
    char* str;
    str = strerror(ret);
    printf("error: %s\n", str);
    }
    printf("exit data: %d\n", *thread_retval);
    printf("回收子线程资源成功\n");

    // 主线程
    for (int i = 0; i < 5; i++) {
    printf("%d\n", i);
    }

    // 打印id
    printf("tid: %ld, parent thread id: %ld\n", tid, pthread_self());
    // 让主线程退出,当至线程退出时,不会影响其他正常运行的线程
    pthread_exit(NULL);
    return 0;
    }
  • pthread_join()中二级指针retval的作用

    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
    /*
    #include <pthread.h>
    功能: 和一个已经终止的线程进行连接。对线程的资源进行释放,回收资源
    这个函数是阻塞函数,调用一次回收一个子线程
    一般在主线程中使用
    参数:
    - thread:指定回收的线程号
    - retval:接受子线程退出时的返回值
    为什么是二级指针:
    返回值
    成功返回0
    失败返回错误号,非零
    int pthread_join(pthread_t thread, void **retval);
    */
    #include <pthread.h>
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>

    int value = 10; // 定义全局变量
    void* callback(void* arg) {
    printf("child thread id: %ld\n", pthread_self());
    // sleep(3);
    // int tmp = 100; //这里是子进程的栈空间,当该线程结束之后是随机值,
    pthread_exit((void*)&value); // 等同于return (void*)&tmp;
    }

    int main() {
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, callback, NULL);

    if (ret != 0) {
    char* str;
    str = strerror(ret);
    printf("error: %s\n", str);
    }
    // 主线程
    for (int i = 0; i < 5; i++) {
    printf("%d\n", i);
    }

    // 主线程调用pthread_join()回收子线程资源
    int* thread_retval;
    ret = pthread_join(tid, (void**)&thread_retval); // 要回收的子线程没有结束,那么程序阻塞

    if (ret != 0) {
    char* str;
    str = strerror(ret);
    printf("error: %s\n", str);
    }
    printf("exit data: %d\n", *thread_retval);
    printf("回收子线程资源成功\n");

    // 打印id
    printf("tid: %ld, parent thread id: %ld\n", tid, pthread_self());
    // 让主线程退出,当至线程退出时,不会影响其他正常运行的线程
    pthread_exit(NULL);
    return 0;
    }
  • 对于二级指针的的理解

    • 回到函数返回值和参数都是一级指针。若该参数同样是一级指针,实际上并不能改变回调函数返回值

线程的分离

  • 使用pthread_detach函数
    • 使得线程在结束时候不用再使用pthread_join
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
/*
#include <pthread.h>
功能: 分离一个线程,被分离的线程在终止的时候会自动的释放资源返回给系统
1.不能多次分离,会产生不可预料的行为
2.不能去连接一个已经分离的线程,会报错
参数
-thread:需要分离的线程的id
返回值
成功返回0
失败返回错误号
int pthread_detach(pthread_t thread);
*/
#include <pthread.h>
#include <stdio.h>
#include <string.h>

void* callback(void* argv) {
printf("child thread id: %ld\n", pthread_self());
return NULL;
}

int main() {
// 创建子线程
pthread_t tid;
int ret = pthread_create(&tid, NULL, callback, NULL);
if (ret != 0) {
char* str;
str = strerror(ret);
printf("errno1: %s\n", str);
}
// 输出主线程和子线程的id
printf("tid: %ld, main thread id: %ld\n", tid, pthread_self());

// 设置子线程分离,分离后,子线程结束后不需要pthread_join函数回收资源
ret = pthread_detach(tid);
if (ret != 0) {
char* str;
str = strerror(ret);
printf("errno2: %s\n", str);
}

// 验证使用join报错
ret = pthread_join(tid, NULL);
if (ret != 0) {
char* str;
str = strerror(ret);
printf("errno3: %s\n", str);
}
// 退出主线程
pthread_exit(NULL);

return 0;
}

线程取消

  • pthread_cancel
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
/*
#include <pthread.h>
功能:取消线程(让线程终止)。
可以终止某个线程的运行
但不是立马终止,而是当子线程执行到一个取消点,线程 才会终止
取消点:系统规定好的一些系统调用,粗略的理解为从用户区到内核区的切换,这个位置称之为取消点


参数
- thread: 要取消的线程的线程号
返回值

int pthread_cancel(pthread_t thread);
*/

#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

void* callback(void* argv) {
printf("child thread tid: %ld\n", pthread_self()); // 切换到内核区时执行到取消点,
for (int i = 0; i < 5; i++) {
printf("child %d\n", i); //for循环不会执行
}
return NULL;
}

int main() {
pthread_t tid;
int ret = pthread_create(&tid, NULL, callback, NULL);
if (ret != 0) {
char* str = strerror(ret);
printf("errno: %s\n", str);
}

// 取消线程
pthread_cancel(tid);

printf("tid : %ld, main thread id: %ld\n", tid, pthread_self());
for (int i = 0; i < 5; i++) {
printf("%d\n", i);
}
pthread_exit(NULL);

return 0;
}

线程属性

  • 相关函数

    • image-20230904201835479
    • image-20230904201931971
  • 使用

    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
    /*
    #include <pthread.h>

    int pthread_attr_init(pthread_attr_t *attr);
    -初始化属性变量
    int pthread_attr_destroy(pthread_attr_t *attr);
    -释放线程属性资源
    int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
    -设置线程分离的状态属性
    int pthread_attr_getdetachstate(const pthread_attr_t *attr,int *detachstate);
    -获取线程分离的状态属性
    */

    #include <pthread.h>
    #include <stdio.h>
    #include <string.h>

    void* callback(void* argv) {
    printf("child thread tid: %ld\n", pthread_self());
    return NULL;
    }

    int main() {
    // 创建一个线程属性变量
    pthread_attr_t attr;
    // 初始化线程属性
    pthread_attr_init(&attr);
    // 设置属性
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    // 创建子线程
    pthread_t tid;
    int ret = pthread_create(&tid, &attr, callback, NULL);
    if (ret != 0) {
    char* str;
    str = strerror(ret);
    printf("errno1: %s\n", str);
    }

    // 获取线程栈的大小
    size_t size;
    pthread_attr_getstacksize(&attr, &size);
    printf("thread stack size:%ld\n", size);

    // 输出主线程和子线程的id
    printf("tid: %ld, main thread id: %ld\n", tid, pthread_self());

    // 释放线程属性资源
    pthread_attr_destroy(&attr);

    // 退出主线程
    pthread_exit(NULL);

    return 0;
    }

线程同步

  • 概念
  • image-20230904210756882
    • 线程同步效率低,但是必要的

互斥锁mutex

  • image-20230904211339210

    • 互斥量的两种状态
      • 已锁定locked
      • 未送定unlocked
  • 只有一个线程能够持有该互斥量,其他线程将遭到阻塞

    image-20230904212201904

  • 互斥量相关操作函数

    • 见下面代码注释中
  • 售票系统案例

    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
    /*
    使用多线程实现买票
    3个窗口,共100张票

    互斥量的类型 pthread_mutex_t
    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
    - 初始化互斥量
    - 参数 :
    - mutex : 需要初始化的互斥量变量
    - attr : 互斥量相关的属性,NULL
    - restrict : C语言的修饰符,被修饰的指针,不能由另外的一个指针进行操作。
    pthread_mutex_t *restrict mutex = xxx;
    pthread_mutex_t * mutex1 = mutex;

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    - 释放互斥量的资源

    int pthread_mutex_lock(pthread_mutex_t *mutex);
    - 加锁,阻塞的,如果有一个线程加锁了,那么其他的线程只能阻塞等待

    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    - 尝试加锁,如果加锁失败,不会阻塞,会直接返回。

    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    - 解锁
    */
    #include <pthread.h>
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>

    // 共享门票,全局变量
    int tickets = 100;

    // 创建一个互斥量,要求是全局变量
    pthread_mutex_t mutex;

    void* sellticket(void* argv) {
    // 卖票
    while (1) {
    // 对共享数据的操作,加锁
    pthread_mutex_lock(&mutex);
    if (tickets > 0) {
    printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
    tickets--;
    } else {
    // 解锁
    pthread_mutex_unlock(&mutex);
    break;
    }
    // 解锁
    pthread_mutex_unlock(&mutex);
    }

    return NULL;
    }

    int main() {
    // 初始化互斥量
    pthread_mutex_init(&mutex, NULL);

    // 创建3个子线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, sellticket, NULL);
    pthread_create(&tid2, NULL, sellticket, NULL);
    pthread_create(&tid3, NULL, sellticket, NULL);

    // 回收子线程资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);

    // 设置线程分离
    // pthread_detach(tid1);
    // pthread_detach(tid2);
    // pthread_detach(tid3);

    // 释放互斥量资源
    pthread_mutex_destroy(&mutex);

    // 退出主线程
    pthread_exit(NULL);
    }

死锁

  • image-20230904214402239
  • 产生死锁的场景
    • 忘记释放锁
    • 重复加锁
    • 多线程多锁,抢占锁资源
      • 一个进程访问多个临界资源

读写锁

  • 一个线程已经持有互斥锁,但是当前线程只是需要读取共享资源,可以赋予其读写锁

  • image-20230905184905556

  • 读写锁的特点

    • 如果有线程读数据,则允许其他线程数据读,不允许写
    • 如果有线程写数据,则其他线程读写操作都不允许
    • 写操作是独占的,写的优先级更高
  • 相关的读写锁操作函数

    image-20230905185626512

  • 虽然互斥锁也能解决读写问题,但是效率很低,使用读写锁能极大的提高效率

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
/*

*/
// 案例:创建8个线程, 操作同一个全局变量
// 3个线程不定时写这个全局,5个线程不定时读这个全局变量

#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

// 共享数据
int num = 1;

// 创建读写锁
pthread_rwlock_t rwlock;

void* writeNum(void* argv) {
while (1) {
pthread_rwlock_wrlock(&rwlock);
num++;
printf("write++, tid: %ld, num: %d\n", pthread_self(), num);
pthread_rwlock_unlock(&rwlock);
usleep(100);
}

return NULL;
}

void* readNum(void* argv) {
while (1) {
pthread_rwlock_rdlock(&rwlock);
printf("===read, tid: %ld, num: %d\n", pthread_self(), num);
pthread_rwlock_unlock(&rwlock);
usleep(100);
}
return NULL;
}

int main() {
// 初始化
pthread_rwlock_init(&rwlock, NULL);

// 创建3个写线程,5个读线程
pthread_t wtids[3], rtids[5];
for (int i = 0; i < 3; i++) {
int ret = pthread_create(&wtids[i], NULL, writeNum, NULL);
}
for (int i = 0; i < 5; i++) {
int ret = pthread_create(&rtids[i], NULL, readNum, NULL);
}

// 设置线程分离
for (int i = 0; i < 3; i++) {
pthread_detach(wtids[i]);
}
for (int i = 0; i < 5; i++) {
pthread_detach(rtids[i]);
}

// 释放锁
pthread_rwlock_destroy(&rwlock);

// 退出主线程
pthread_exit(NULL);

return 0;
}

生产者消费者

  • 三要素
    • 生产者
    • 消费者
    • 容器
  • 当容器装满
    • 生产者进程阻塞
  • 当容器空
    • 消费者进程阻塞

条件变量

  • image-20230905211311914
    • 满足某个条件执行操作
  • 生产者消费者和条件变量的示例
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
/*
生产者消费者模型
使用了条件变量
*/

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// 链表当做容器
struct Node {
int num;
struct Node* next;
};

// 创建互斥量
pthread_mutex_t mutex;
// 创建条件变量
pthread_cond_t cond;

// 头结点
struct Node* head = NULL;

void* producer(void* arg) {
while (1) {
pthread_mutex_lock(&mutex);
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->next = head;
head = newNode;
newNode->num = rand() % 1000;
printf("add node, num: %d, tid: %ld\n", newNode->num, pthread_self());
// 只要生产了一个,就通知消费者消费
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
usleep(100);
}
return NULL;
}

void* customer(void* arg) {
while (1) {
pthread_mutex_lock(&mutex);
// 判断是否有数据
if (head == NULL) {
// 容器空了,阻塞
// 当wait函数调用阻塞时会解锁,释放mutex,当不阻塞时,继续向下执行时会重新加锁
pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
} else {
struct Node* tmp = head;
printf("delete node, num: %d, tid: %ld\n", tmp->num, pthread_self());
head = head->next;
free(tmp);
pthread_mutex_unlock(&mutex);
usleep(100);
}
}
return NULL;
}

// 创建5个生产者线程和5个消费者线程
int main() {
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_t ptids[5], ctids[5];
for (int i = 0; i < 5; i++) {
pthread_create(&ptids[i], NULL, producer, NULL);
pthread_create(&ctids[i], NULL, customer, NULL);
}

for (int i = 0; i < 5; i++) {
pthread_detach(&ptids[i]);
pthread_detach(&ctids[i]);
}

while (1) {
sleep(10);
}

pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
pthread_exit(NULL);
return 0;
}

信号量机制

  • 信号量的类型 sem_t

    image-20230905214142063

    • 阻塞线程,不能保证线程安全
  • 信号量的使用

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/*
信号量的类型 sem_t
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 初始化信号量
- 参数:
- sem : 信号量变量的地址
- pshared : 0 用在线程间 ,非0 用在进程间
- value : 信号量中的值

int sem_destroy(sem_t *sem);
- 释放资源

int sem_wait(sem_t *sem);
- 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞

int sem_trywait(sem_t *sem);

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
- 对信号量解锁,调用一次对信号量的值+1

int sem_getvalue(sem_t *sem, int *sval);

sem_t psem;
sem_t csem;
init(psem, 0, 8);
init(csem, 0, 0);

producer() {
sem_wait(&psem);
sem_post(&csem)
}

customer() {
sem_wait(&csem);
sem_post(&psem)
}

*/

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>

// 创建一个互斥量
pthread_mutex_t mutex;
// 创建两个信号量
sem_t psem;
sem_t csem;

struct Node{
int num;
struct Node *next;
};

// 头结点
struct Node * head = NULL;

void * producer(void * arg) {

// 不断的创建新的节点,添加到链表中
while(1) {
sem_wait(&psem);
pthread_mutex_lock(&mutex);
struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
newNode->next = head;
head = newNode;
newNode->num = rand() % 1000;
printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
pthread_mutex_unlock(&mutex);
sem_post(&csem);
}

return NULL;
}

void * customer(void * arg) {

while(1) {
sem_wait(&csem);
pthread_mutex_lock(&mutex);
// 保存头结点的指针
struct Node * tmp = head;
head = head->next;
printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
free(tmp);
pthread_mutex_unlock(&mutex);
sem_post(&psem);

}
return NULL;
}

int main() {

pthread_mutex_init(&mutex, NULL);
sem_init(&psem, 0, 8);
sem_init(&csem, 0, 0);

// 创建5个生产者线程,和5个消费者线程
pthread_t ptids[5], ctids[5];

for(int i = 0; i < 5; i++) {
pthread_create(&ptids[i], NULL, producer, NULL);
pthread_create(&ctids[i], NULL, customer, NULL);
}

for(int i = 0; i < 5; i++) {
pthread_detach(ptids[i]);
pthread_detach(ctids[i]);
}

while(1) {
sleep(10);
}

pthread_mutex_destroy(&mutex);

pthread_exit(NULL);

return 0;
}

网络编程

网络结构模式

C/S结构


服务器 - 客户机,即 Client - Server(C/S)结构。C/S 结构通常采取两层结构。服务器负责数据的

管理,客户机负责完成与用户的交互任务。客户机是因特网上访问别人信息的机器,服务器则是提

供信息供人访问的计算机。

客户机通过局域网与服务器相连,接受用户的请求,并通过网络向服务器提出请求,对数据库进行

操作。服务器接受客户机的请求,将数据提交给客户机,客户机将数据进行计算并将结果呈现给用

户。服务器还要提供完善安全保护及对数据完整性的处理等操作,并允许多个客户机同时访问服务

器,这就对服务器的硬件处理数据能力提出了很高的要求。

在C/S结构中,应用程序分为两部分:服务器部分和客户机部分。服务器部分是多个用户共享的信

息与功能,执行后台服务,如控制共享数据库的操作等;客户机部分为用户所专有,负责执行前台

功能,在出错提示、在线帮助等方面都有强大的功能,并且可以在子程序间自由切换。

  • 优点

    1. 能充分发挥客户端 PC 的处理能力,很多工作可以在客户端处理后再提交给服务器,所以 C/S 结构

    客户端响应速度快;

    1. 操作界面漂亮、形式多样,可以充分满足客户自身的个性化要求;
    2. C/S 结构的管理信息系统具有较强的事务处理能力,能实现复杂的业务流程;
    3. 安全性较高,C/S 一般面向相对固定的用户群,程序更加注重流程,它可以对权限进行多层次校

    验,提供了更安全的存取模式,对信息安全的控制能力很强,一般高度机密的信息系统采用 C/S 结

    构适宜

  • 缺点

    1. 客户端需要安装专用的客户端软件。首先涉及到安装的工作量,其次任何一台电脑出问题,如病

    毒、硬件损坏,都需要进行安装或维护。系统软件升级时,每一台客户机需要重新安装,其维护和

    升级成本非常高;

    1. 对客户端的操作系统一般也会有限制,不能够跨平台。

B/S结构

B/S 结构(Browser/Server,浏览器/服务器模式),是 WEB 兴起后的一种网络结构模式,WEB

浏览器是客户端最主要的应用软件。这种模式统一了客户端,将系统功能实现的核心部分集中到服

务器上,简化了系统的开发、维护和使用。客户机上只要安装一个浏览器,如 Firefox 或 Internet

Explorer,服务器安装 SQL Server、Oracle、MySQL 等数据库。浏览器通过 Web Server 同数据

库进行数据交互。

  • 优点

    B/S 架构最大的优点是总体拥有成本低、维护方便、 分布性强、开发简单,可以不用安装任何专门的软

    件就能实现在任何地方进行操作,客户端零维护,系统的扩展非常容易,只要有一台能上网的电脑就能

    使用。

  • 缺点

    1. 通信开销大、系统和数据的安全性较难保障;
    2. 个性特点明显降低,无法实现具有个性化的功能要求;
    3. 协议一般是固定的:http/https
    4. 客户端服务器端的交互是请求-响应模式,通常动态刷新页面,响应速度明显降低。

MAC地址

网卡是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件,又称为网络适配器或网

络接口卡NIC。其拥有 MAC 地址,属于 OSI 模型的第 2 层,它使得用户可以通过电缆或无线相互

连接。每一个网卡都有一个被称为 MAC 地址的独一无二的 48 位串行号。网卡的主要功能:1.数

据的封装与解封装、2.链路管理、3.数据编码与译码。


  • MAC地址

MAC 地址(Media Access Control Address),直译为媒体存取控制位址,也称为局域网地址、

以太网地址、物理地址或硬件地址,它是一个用来确认网络设备位置的位址,由网络设备制造商生

产时烧录在网卡中(固定死)。在 OSI 模型中,第三层网络层负责 IP 地址,第二层数据链路层则负责 MAC

位址 。MAC 地址用于在网络中唯一标识一个网卡,一台设备若有一或多个网卡,则每个网卡都需

要并会有一个唯一的 MAC 地址。(比如说我的笔记本有以太网卡和无线网卡,两个网卡的MAC地址不同)

MAC 地址的长度为 48 位(6个字节),通常表示为 12 个 16 进制数,如:00-16-EA-AE-3C-40 就是一个MAC 地址,其中前 3 个字节,16 进制数 00-16-EA 代表网络硬件制造商的编号,它由IEEE(电气与电子工程师协会)分配,而后 3 个字节,16进制数 AE-3C-40 代表该制造商所制造的某个网络产品(如网卡)的系列号。只要不更改自己的 MAC 地址,MAC 地址在世界是唯一的。

形象地说,MAC 地址就如同身份证上的身份证号码,具有唯一性。

IP地址

IP 协议是为计算机网络相互连接进行通信而设计的协议。在因特网中,它是能使连接到网上的所

有计算机网络实现相互通信的一套规则,规定了计算机在因特网上进行通信时应当遵守的规则。任

何厂家生产的计算机系统,只要遵守 IP 协议就可以与因特网互连互通。各个厂家生产的网络系统

和设备,如以太网、分组交换网等,它们相互之间不能互通,不能互通的主要原因是因为它们所传

送数据的基本单元(技术上称之为“帧”)的格式不同。IP 协议实际上是一套由软件程序组成的协议

软件,它把各种不同“帧”统一转换成“IP 数据报”格式,这种转换是因特网的一个最重要的特点,使

所有各种计算机都能在因特网上实现互通,即具有“开放性”的特点。正是因为有了 IP 协议,因特

网才得以迅速发展成为世界上最大的、开放的计算机通信网络。因此,IP 协议也可以叫做“因特网

协议”。

IP 地址(Internet Protocol Address)是指互联网协议地址,又译为网际协议地址。IP 地址是 IP

协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址

此来屏蔽物理地址的差异。

IPv4地址是一个 32 位的二进制数,通常被分割为 4 个“ 8 位二进制数”(也就是 4 个字节)。IP 地址

通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是 0~255 之间的十进制整数。

例:点分十进IP地址(100.4.5.6),实际上是 32 位二进制数

(01100100.00000100.00000101.00000110)。

IP地址编址方式

最初设计互联网络时,为了便于寻址以及层次化构造网络,每个 IP 地址包括两个标识码(ID),即网络

ID 和主机 ID。同一个物理网络上的所有主机都使用同一个网络 ID,网络上的一个主机(包括网络上工

作站,服务器和路由器等)有一个主机 ID 与其对应。Internet 委员会定义了 5 种 IP 地址类型以适合不

同容量的网络,即 A 类~ E 类。

其中 A、B、C 3类(如下表格)由 InternetNIC 在全球范围内统一分配,D、E 类为特殊地址。

image-20230906094959196

  • A类IP地址
     一个 A 类 IP 地址是指, 在 IP 地址的四段号码中,第一段号码为网络号码,剩下的三段号码为本地计算

 机的号码。如果用二进制表示 IP 地址的话,A 类 IP 地址就由 1 字节的网络地址和 3 字节主机地址组

 成,**网络地址的最高位必须是“0”**。A 类 IP 地址中网络的标识长度为 8 位,主机标识的长度为 24 位,A

 类网络地址数量较少,有 126 个网络,每个网络可以容纳主机数达 1600 多万台。

 A 类 IP 地址 地址范围 1.0.0.1 - 126.255.255.254(二进制表示为:00000001 00000000 00000000

 00000001 - 01111111 11111111 11111111 11111110)。最后一个是广播地址(全1)。

 A 类 IP 地址的子网掩码为 255.0.0.0,每个网络支持的最大主机数为 256 的 3 次方 - 2 = 16777214 台。
  • B 类IP地址

一个 B 类 IP 地址是指,在 IP 地址的四段号码中,前两段号码为网络号码。如果用二进制表示 IP 地址的

话,B 类 IP 地址就由 2 字节的网络地址和 2 字节主机地址组成,网络地址的最高位必须是“10”。B 类 IP

地址中网络的标识长度为 16 位,主机标识的长度为 16 位,B 类网络地址适用于中等规模的网络,有

16384 个网络,每个网络所能容纳的计算机数为 6 万多台。

B 类 IP 地址地址范围 128.0.0.1 - 191.255.255.254 (二进制表示为:10000000 00000000 00000000

00000001 - 10111111 11111111 11111111 11111110)。 最后一个是广播地址。

B 类 IP 地址的子网掩码为 255.255.0.0,每个网络支持的最大主机数为 256 的 2 次方 - 2 = 65534 台

  • C 类IP地址

一个 C 类 IP 地址是指,在 IP 地址的四段号码中,前三段号码为网络号码,剩下的一段号码为本地计算

机的号码。如果用二进制表示 IP 地址的话,C 类 IP 地址就由 3 字节的网络地址和 1 字节主机地址组

成,网络地址的最高位必须是“110”。C 类 IP 地址中网络的标识长度为 24 位,主机标识的长度为 8 位,

C 类网络地址数量较多,有 209 万余个网络。适用于小规模的局域网络,每个网络最多只能包含254台

计算机。

C 类 IP 地址范围 192.0.0.1-223.255.255.254 (二进制表示为: 11000000 00000000 00000000

00000001 - 11011111 11111111 11111111 11111110)。

C类IP地址的子网掩码为 255.255.255.0,每个网络支持的最大主机数为 256 - 2 = 254 台。

  • 特殊的网址

每一个字节都为 0 的地址( “0.0.0.0” )对应于当前主机

IP 地址中的每一个字节都为 1 的 IP 地址( “255.255.255.255” )是当前子网的广播地址

IP 地址中凡是以 “11110” 开头的 E 类 IP 地址都保留用于将来和实验使用。

IP地址中不能以十进制 “127” 作为开头,该类地址中数字 127.0.0.1 到 127.255.255.255 用于回路测

试,如:127.0.0.1可以代表本机IP地址。

子网掩码

子网掩码(subnet mask)又叫网络掩码、地址掩码、子网络遮罩,它是一种用来指明一个 IP 地

址的哪些位标识的是主机所在的子网,以及哪些位标识的是主机的位掩码。子网掩码不能单独存

在,它必须结合 IP 地址一起使用。子网掩码只有一个作用,就是将某个 IP 地址划分成网络地址和

主机地址两部分。

子网掩码是一个 32 位地址,用于屏蔽 IP 地址的一部分以区别网络标识和主机标识,并说明该 IP

地址是在局域网上,还是在广域网上

  • 子网掩码是在 IPv4 地址资源紧缺的背景下为了解决 lP 地址分配而产生的虚拟 lP 技术,通过子网掩码将

    A、B、C 三类地址划分为若干子网,从而显著提高了 IP 地址的分配效率,有效解决了 IP 地址资源紧张

    的局面。另一方面,在企业内网中为了更好地管理网络,网管人员也利用子网掩码的作用,人为地将一

    个较大的企业内部网络划分为更多个小规模的子网,再利用三层交换机的路由功能实现子网互联,从而

    有效解决了网络广播风暴和网络病毒等诸多网络管理方面的问题。在大多数的网络教科书中,一般都将子网掩码的作用描述为通过逻辑运算,将 IP 地址划分为网络标识

    (Net.ID) 和主机标识(Host.ID),只有网络标识相同的两台主机在无路由的情况下才能相互通信。

    根据 RFC950 定义,子网掩码是一个 32 位的 2 进制数, 其对应网络地址的所有位都置为 1,对应于主

    机地址的所有位置都为 0。子网掩码告知路由器,地址的哪一部分是网络地址,哪一部分是主机地址,

    使路由器正确判断任意 IP 地址是否是本网段的,从而正确地进行路由。网络上,数据从一个地方传到另

    外一个地方,是依靠 IP 寻址。从逻辑上来讲,是两步的。第一步,从 IP 中找到所属的网络,好比是去

    找这个人是哪个小区的;第二步,再从 IP 中找到主机在这个网络中的位置,好比是在小区里面找到这个

    人。

    子网掩码的设定必须遵循一定的规则。与二进制 IP 地址相同,子网掩码由 1 和 0 组成,且 1 和 0 分别

    连续。子网掩码的长度也是 32 位,左边是网络位,用二进制数字 “1” 表示,1 的数目等于网络位的长

    度;右边是主机位,用二进制数字 “0” 表示,0 的数目等于主机位的长度。这样做的目的是为了让掩码

    与 IP 地址做按位与运算时用 0 遮住原主机数,而不改变原网络段数字,而且很容易通过 0 的位数确定子

    网的主机数( 2 的主机位数次方 - 2,因为主机号全为 1 时表示该网络广播地址,全为 0 时表示该网络

    的网络号,这是两个特殊地址)。通过子网掩码,才能表明一台主机所在的子网与其他子网的关系,使

    网络正常工作。

端口

  • 标记计算机中进程的唯一编号

“端口” 是英文 port 的意译,可以认为是设备与外界通讯交流的出口。端口可分为虚拟端口和物理

端口,其中虚拟端口指计算机内部或交换机路由器内的端口,不可见,是特指TCP/IP协议中的端

口,是逻辑意义上的端口。例如计算机中的 80 端口、21 端口、23 端口等。物理端口又称为接

口,是可见端口,计算机背板的 RJ45 网口,交换机路由器集线器等 RJ45 端口。电话使用 RJ11 插

口也属于物理端口的范畴。

如果把 IP 地址比作一间房子,端口就是出入这间房子的门。真正的房子只有几个门,但是一个 IP

地址的端口可以有 65536(即:2^16)个之多!端口是通过端口号来标记的,端口号只有整数,

范围是从 0 到65535(2^16-1)。

  • 端口类型

    ​ 1.周知端口(Well Known Ports)

    周知端口是众所周知的端口号,也叫知名端口、公认端口或者常用端口,范围从 0 到 1023,它们紧密

    绑定于一些特定的服务。例如 80 端口分配给 WWW 服务,21 端口分配给 FTP 服务,23 端口分配给

    Telnet服务等等。我们在 IE 的地址栏里输入一个网址的时候是不必指定端口号的,因为在默认情况下

    WWW 服务的端口是 “80”。网络服务是可以使用其他端口号的,如果不是默认的端口号则应该在地址栏

    上指定端口号,方法是在地址后面加上冒号“:”(半角),再加上端口号。比如使用 “8080” 作为 WWW

    服务的端口,则需要在地址栏里输入“网址:8080”。但是有些系统协议使用固定的端口号,它是不能被改

    变的,比如 139 端口专门用于 NetBIOS 与 TCP/IP 之间的通信,不能手动改变。

    ​ 2.注册端口(Registered Ports)

    端口号从 1024 到 49151,它们松散地绑定于一些服务,分配给用户进程或应用程序,这些进程主要是

    用户选择安装的一些应用程序,而不是已经分配好了公认端口的常用程序。这些端口在没有被服务器资

    源占用的时候,可以用用户端动态选用为源端口。

    ​ 3.动态端口 / 私有端口(Dynamic Ports / Private Ports)

    动态端口的范围是从 49152 到 65535。之所以称为动态端口,是因为它一般不固定分配某种服务,而是

    动态分配。

OSI模型

  • OSI七层模型

    七层模型,亦称 OSI(Open System Interconnection)参考模型,即开放式系统互联。参考模型

    是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系,一般称为 OSI 参

    考模型或七层模型。

    它是一个七层的、抽象的模型体,不仅包括一系列抽象的术语或概念,也包括具体的协议

    image-20230906140957373

    1. 物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率

    等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为

    1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。

    1. 数据链路层:建立逻辑连接、进行硬件地址寻址差错校验等功能。定义了如何让格式化数据以帧

    为单位进行传输,以及如何让控制对物理介质的访问。将比特组合成字节进而组合成帧,用MAC地

    址访问介质。

    1. 网络层:进行逻辑地址寻址,在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选

    择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的

    层。

    1. 传输层:定义了一些传输数据的协议和端口号( WWW 端口 80 等),如:TCP(传输控制协议,

    传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与

    TCP 特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如 QQ 聊天数据就是通过这种方

    式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这

    一层数据叫做段。

    1. 会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间

    发起会话或者接受会话请求。保存了连接信息,下次再次通信时不需要从底层重新寻找

    1. 表示层:数据的表示、安全、压缩。主要是进行对接收的数据进行解释、加密与解密、压缩与解压

    缩等(也就是把计算机能够识别的东西转换成人能够能识别的东西(如图片、声音等)。

    1. 应用层:网络服务与最终用户的一个接口。这一层为用户的应用程序(例如电子邮件、文件传输和

    终端仿真)提供网络服务。

  • OSI七层模型

现在 Internet(因特网)使用的主流协议族是 TCP/IP 协议族,它是一个分层、多协议的通信体

系。TCP/IP协议族是一个四层协议系统,自底而上分别是数据链路层、网络层、传输层和应用

层。每一层完成不同的功能,且通过若干协议来实现,上层协议使用下层协议提供的服务

  • image-20230906201217745

  • TCP/IP 协议在一定程度上参考了 OSI 的体系结构。OSI 模型共有七层,从下到上分别是物理层、数据链

    路层、网络层、传输层、会话层、表示层和应用层。但是这显然是有些复杂的,所以在 TCP/IP 协议中,

    它们被简化为了四个层次。

    (1)应用层、表示层、会话层三个层次提供的服务相差不是很大,所以在 TCP/IP 协议中,它们被合并

    为应用层一个层次。

    (2)由于传输层和网络层在网络协议中的地位十分重要,所以在 TCP/IP 协议中它们被作为独立的两个

    层次。

    (3)因为数据链路层和物理层的内容相差不多,所以在 TCP/IP 协议中它们被归并在网络接口层一个层

    次里。只有四层体系结构的 TCP/IP 协议,与有七层体系结构的 OSI 相比要简单了不少,也正是这样,

    TCP/IP 协议在实际的应用中效率更高,成本更低。

    image-20230906201507452

协议

协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定。如怎么样建立连

接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流。它的三要素是:

法、语义、时序

为了使数据在网络上从源到达目的,网络通信的参与方必须遵循相同的规则,这套规则称为协议

(protocol),它最终体现为在网络上传输的数据包的格式

协议往往分成几个层次进行定义,分层定义是为了使某一层协议的改变不影响其他层次的协议

  • 语法:什么操作

  • 语义:什么意思

  • 时序(同步):顺序

  • 常见的协议

    应用层常见的协议有:FTP协议(File Transfer Protocol 文件传输协议)、HTTP协议(Hyper Text

    Transfer Protocol 超文本传输协议)、NFS(Network File System 网络文件系统)。

    传输层常见协议有:TCP协议(Transmission Control Protocol 传输控制协议)、UDP协议(User

    Datagram Protocol 用户数据报协议)。

    网络层常见协议有:IP 协议(Internet Protocol 因特网互联协议)、ICMP 协议(Internet Control Message Protocol 因特网控制报文协议)、IGMP 协议(Internet Group Management Protocol 因特

    网组管理协议)。

    网络接口层常见协议有:ARP协议(Address Resolution Protocol 地址解析协议)、RARP协议

    (Reverse Address Resolution Protocol 反向地址解析协议)

UDP协议

image-20230906204136771

TCP协议

image-20230906204227880

IP协议

image-20230906204435031

以太网帧协议

image-20230906205038870

ARP协议

image-20230906205054358

网络通信过程

封装

上层协议是如何使用下层协议提供的服务的呢?其实这是通过封装(encapsulation)实现的。应用程序

数据在发送到物理网络上之前,将沿着协议栈从上往下依次传递。每层协议都将在上层数据的基础上加

上自己的头部信息(有时还包括尾部信息),以实现该层的功能,这个过程就称为封装。

  • 什么是封装

    image-20230906210915197

分用

当帧到达目的主机时,将沿着协议栈自底向上依次传递。各层协议依次处理帧中本层负责的头部数据,

以获取所需的信息,并最终将处理后的帧交给目标应用程序。这个过程称为分用(demultiplexing)。

分用是依靠头部信息中的类型字段实现的。

  • 通信示意图

    image-20230906213049487

    • 怎么根据IP地址查找MAC地址?
      • ARP协议
        • 通过IP地址查找MAC地址
  • ARP协议示意

    image-20230906214600378

  • ARP请求包

image-20230906214710955

  • 响应时因为知道目的端的MAC地址,直接传送,不需要广播
    • 注意ARP报文端有些需要改变

socket

  • 一系列的接口

所谓 socket(套接字),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象

一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处

的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,

是应用程序与网络协议进行交互的接口。

socket 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概

念。它是网络环境中进程间通信的 API,也是可以被命名和寻址的通信端点,使用中的每一个套接

字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在

主机的 socket 中,该 socket 通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台

主机的 socket 中,使对方能够接收到这段信息。socket 是由 IP 地址和端口结合的,提供向应用

层进程传送数据包的机制。

socket 本身有“插座”的意思,在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为

内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接

字。与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文

件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传

递。

  • image-20230906223159019

  • 套接字通信分为两部分

    • 服务器端
      • 被动接受客户端连接
    • 客户端
      • 主动向服务器端发起连接
  • socket是一套通信的接口,Linux和windows都有,有一些细微的差别

字节序

  • 简介:

现代 CPU 的累加器一次都能装载(至少)4 字节(这里考虑 32 位机),即一个整数。那么这 4

字节在内存中排列的顺序将影响它被累加器装载成的整数的值,这就是字节序问题。在各种计算机

体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问

题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如

套接字通信分两部分:

- 服务器端:被动接受连接,一般不会主动发起连接

- 客户端:主动向服务器发起连接

socket是一套通信的接口,Linux 和 Windows 都有,但是有一些细微的差别。果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。

字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数

据当然就无需谈顺序的问题了)。

字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian)。大端字节序是指一个整

数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地

址处;小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地

址处。

  • 举例

    • 小端字节序

      image-20230907125254993

    • 大端字节序

      image-20230907125308343

  • 计算机一般采用的是小端字节序

  • 测试自己计算机的字节序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
字节序:字节在内存中存储的顺序
小端字节序: 数据的高位字节存储在内存的高位地址,低位字节存储在内存的低位地址
大端字节序: 数据的低位字节存储在内存的高位地址,高位字节存储在内存的低位地址
*/

// 通过代码检测当前主机的字节序
#include <stdio.h>

int main() {
union {
short value; // 2B
char bytes[sizeof(short)]; // 2B
} test;
test.value = 0x0102;
if (test.bytes[0] == 1 && test.bytes[1] == 2) {
printf("大端字节序\n");
} else if (test.bytes[0] == 2 && test.bytes[1] == 1) {
printf("小端字节序\n");
} else {
printf("未知\n");
}
}

字节序转换函数

当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。协议族 地址族 描述

PF_UNIX AF_UNIX UNIX本地域协议族PF_INET、AF_INET、TCP/IPv4协议族、PF_INET6、AF_INET6 、TCP/IPv6协议族

网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。

  • 规定网络字节序都是大端,主机字节序视自己计算机情况而定

网络通信时,需要将主机字节序转换成网络字节序(大端)

另一段获取到数据之后根据情况将网络字节序转换成主机字节序

1
2
3
4
5
h - host 主机,主机字节序 
to - 转换成什么
n - network 网络字节序
s - short unsigned short
l - long unsigned int
1
2
3
4
5
6
7
#include <arpa/inet.h> 
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort); // 网络字节序 - 主机字节序
// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); // 网络字节序 - 主机字节序
  • 区别:数据类型不一样。short类型适合用于端口的转换,long类型适合用于IP的转换

  • 四个函数的使用

    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
    /*
    // 转换端口
    uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
    uint16_t ntohs(uint16_t netshort); // 网络字节序 - 主机字节序
    // 转IP
    uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
    uint32_t ntohl(uint32_t netlong); // 网络字节序 - 主机字节序
    */

    #include <arpa/inet.h>
    #include <stdio.h>

    int main() {
    // htons
    unsigned short a = 0x0102; // 2字节
    printf("a: %x\n", a);
    unsigned short b = htons(a);
    printf("b: %x\n", b);

    printf("===============\n");

    // htonl 转IP
    char buf[4] = {192, 168, 1, 100};
    int num = *(int*)buf; // 转换成int* 取的时候是读取四个字节
    int sum = htonl(num);
    unsigned char* p = (char*)&sum;
    printf("%d %d %d %d\n", *p, *(p + 1), *(p + 2), *(p + 3));

    printf("===============\n");

    // ntohl
    unsigned char buf1[4] = {1, 1, 168, 192};
    int num1 = *(int*)buf1;
    int sum1 = ntohl(num1);
    unsigned char* p1 = (unsigned char*)&sum1;
    printf("%d %d %d %d\n", *p1, *(p1 + 1), *(p1 + 2), *(p1 + 3));

    // ntohs
    unsigned short c = 0x0304;
    printf("c: %x\n", c);
    unsigned short d = ntohs(c);
    printf("d: %x\n", d);

    return 0;
    }

socket地址

  • socket是一系列网络通信的API

客户端 -> 服务器(IP, Port)

socket其实就是一个结构体,封装端口号和IP的信息。后面socket相关的API需要使用这个socket地址

  • 通过socket地址

    • socket网络编程接口中表示socket地址是结构体sockaddr,其定义如下
    1
    2
    3
    4
    5
    6
    #include <bits/socket.h> 
    struct sockaddr {
    sa_family_t sa_family;
    char sa_data[14];
    };
    typedef unsigned short int sa_family_t;
  • sa_family : 地址族类型sa_family_t的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称domain)和对应的地址族表示如下

协议族 地址族 描述
PF_UNIX AF_UNIX UNIX本地域协议族
PF_INET AF_INET TCP/IPv4协议族
PF_INET6 AF_INET6 TCP/IPv6协议族
  • ​ *宏 PF_ * 和 AF_ * 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,**所以二者通常混

    用。

  • sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所

    示:

协议族 地址值含义和长度
PF_UNIX 文件的路径名,长度可达到108字节
PF_INET 16 bit 端口号和 32 bit IPv4 地址,共 6 字节
PF_INET6 16 bit 端口号,32 bit 流标识,128 bit IPv6 地址,32 bit 范围 ID,共 26 字节

由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新 的

通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。

1
2
3
4
5
6
7
#include <bits/socket.h> 
struct sockaddr_storage {
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[ 128 - sizeof(__ss_align) ];
};
typedef unsigned short int sa_family_t;

专用的socket地址 struct socket_in

image-20230908141036871

  • image-20230908141258677
    • 专用socket地址类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换),因为所有socket编程接口使用的地址参数类型都是sockaddr

IP地址转换函数

  • 两个功能

    • 将字符串IP和整数相互之间的转换
    • 主机字节序和网络字节序之间的转换
  • 通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用

    十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录

    日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。下面 3 个函数可用于用点分十进制字

    符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:

1
2
3
4
#include <arpa/inet.h> 
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
  • 下面这对更新的函数跟上面3个函数有同样的功能,并且他们同时适用IPv4和IPv6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <arpa/inet.h> 
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面

// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char * inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src:要转换的IP的整数的地址
st:转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的容量)
返回值:返回转换后的数据的地址(字符串),和dst是一样的
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
/*
#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面

// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char * inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src:要转换的IP的整数的地址
st:转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的容量)
返回值:返回转换后的数据的地址(字符串),和dst是一样的
*/

#include <arpa/inet.h>
#include <stdio.h>

int main() {
// 创建一个点分十进制的IP字符串
char buf[] = "192.168.1.4";
unsigned int num;
// 将点分十进制的IP字符串转换成网络字节序的整数
inet_pton(AF_INET, buf, &num); // AF_INET 代表IPv4
unsigned char* p = (unsigned char*)&num;
printf("%d %d %d %d\n", *p, *(p + 1), *(p + 2), *(p + 3));

// 将网络字节序的IP整数转换成点分十进制的IP字符串
char ip[16] = "";

const char* str = inet_ntop(AF_INET, &num, ip, sizeof(ip));
printf("str: %s\n", str);
printf("ip: %s\n", ip); // str 和 IP的地址是相同的
return 0;
}

TCP通信流程

  • TCP和UDP

    image-20230908192756695

  • TCP通信流程

    image-20230908192859209

  • 服务器端

被动接受链接

  • 1.创建一个监听套接字
    • 监听有客户端的链接
    • 套接字:这个套接字其实就是一个文件描述符
  • 2.将这个监听的文件描述符和本地的IP和端口绑定
    • IP和端口就是服务器的地址信息
    • 客户端连接服务器的时候使用的就是这个IP和端口
  • 3.设置监听,监听的fd开始工作
    • 监听的是服务器的读缓冲区是否有数据
  • 4.阻塞等待,当有客户端发起连接,结束阻塞,接受客户端的连接,会得到一个和客户端通信的套接字
    • 通信的文件描述符和监听的文件描述符不同
  • 5.通信
    • 接受数据
    • 发送数据
  • 6.通信结束,断开连接
  • TCP客户端通信流程
  • 1.创建用于通信的套接字(文件描述符)
    • 不需要绑定端口和IP
  • 2.连接服务器,需要指定连接的服务器的IP和端口
  • 3.通信
    • 接受数据
    • 发送数据
  • 4 .通信结束,断开连接

socket函数

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
/*
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
#include <sys/socket.h>
#include <sys/types.h>

int socket(int domain, int type, int protocol);
功能:创建一个套接字
参数
-domain: 协议族
Name Purpose Man page
AF_UNIX AF_LOCAL Local communication unix(7)
AF_INET6 IPv6 Internet protocols
AF_INET IPv4 Internet protocols ip(7)
-type: 通信过程中使用的协议类型
SOCK_STREAM 流式协议
SOCK_DGRAM 报式协议
- protocol: 具体的协议,一般写0,
SOCK_STREAM流式协议默认使用TCP
SOCK_DGRAM 报式协议默认使用UDP
返回值: 返回文件描述符,操作的就是内核缓冲区
成功返回fd
失败返回 -1


int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能: 绑定,将fd和本地的IP + 端口进行绑定
参数:
-sockfd: 通过socket函数得到的文件描述符
-addr: 需要绑定的socket地址,这个地址就封装了IP和端口号的信息
-addrlen: addr结构体占的内存大小

int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
功能: 监听这个套接字上的连接
参数
- sockfd: 通过socket函数得到的文件描述符
-backlog: 未连接和已经链接的套接字的和的最大值,一般为4096
成功返回0,失败返回-1

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:接受客户端连接,默认是一个阻塞等待连接,阻塞等待客户端连接
参数
-sockfd: 用于监听的文件描述符
-addr: 传出参数,记录了连接成功后客户端的地址信息(IP,port)
-addrlen: 指定第二个参数的文件大小
返回值:
成功返回用于通信的文件描述符
失败返回-1

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能: 客户端连接服务器
参数
-sockfd: 用于通信的文件描述符
-addr: 客户端要连接服务器的地址信息
-addrlen: 第二个参数的内存大小
返回值
成功返回0
失败返回-1
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据

*/
  • write
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
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("socket");
exit(0);
}
// 2.连接服务器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, " 192.168.203.59", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);

int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if (ret == -1) {
perror("connect");
exit(0);
}

// 通信
char recvBuf[1024] = {0};
while (1) {
// 给服务器发送数据
char* sendBuf = "hello , i am client";
write(fd, sendBuf, strlen(sendBuf));
// 读数据
int len = read(fd, recvBuf, sizeof(recvBuf));
if (len == -1) {
perror("read");
exit(0);
} else if (len > 0) {
printf("recv server data: %s\n", recvBuf);
} else if (len == 0) {
printf("server close..\n");
break;
}
sleep(1);
}

close(fd);
return 0;
}
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/*
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
#include <sys/socket.h>
#include <sys/types.h>

int socket(int domain, int type, int protocol);
功能:创建一个套接字
参数
-domain: 协议族
Name Purpose Man page
AF_UNIX AF_LOCAL Local communication unix(7)
AF_INET6 IPv6 Internet protocols
AF_INET IPv4 Internet protocols ip(7)
-type: 通信过程中使用的协议类型
SOCK_STREAM 流式协议
SOCK_DGRAM 报式协议
- protocol: 具体的协议,一般写0,
SOCK_STREAM流式协议默认使用TCP
SOCK_DGRAM 报式协议默认使用UDP
返回值: 返回文件描述符,操作的就是内核缓冲区
成功返回fd
失败返回 -1


int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能: 绑定,将fd和本地的IP + 端口进行绑定
参数:
-sockfd: 通过socket函数得到的文件描述符
-addr: 需要绑定的socket地址,这个地址就封装了IP和端口号的信息
-addrlen: addr结构体占的内存大小

int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
功能: 监听这个套接字上的连接
参数
- sockfd: 通过socket函数得到的文件描述符
-backlog: 未连接和已经链接的套接字的和的最大值,一般为4096
成功返回0,失败返回-1

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:接受客户端连接,默认是一个阻塞等待连接,阻塞等待客户端连接
参数
-sockfd: 用于监听的文件描述符
-addr: 传出参数,记录了连接成功后客户端的地址信息(IP,port)
-addrlen: 指定第二个参数的文件大小
返回值:
成功返回用于通信的文件描述符
失败返回-1

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能: 客户端连接服务器
参数
-sockfd: 用于通信的文件描述符
-addr: 客户端要连接服务器的地址信息
-addrlen: 第二个参数的内存大小
返回值
成功返回0
失败返回-1
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据

*/

#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {
// 1.创建监听套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1) {
perror("socket");
exit(0);
}
// 2.绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, " 192.168.203.59", saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret == -1) {
perror("bind");
exit(0);
}

// 3.监听
ret = listen(lfd, 8);
if (ret == -1) {
perror("listen");
exit(0);
}

// 4.接收客户端连接
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);
if (cfd == -1) {
perror("accept");
exit(0);
}

// 输出客户端信息,将其转换成主机字节序
char clientIP[16];
// 获取IP
inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
// 获取端口
unsigned short clientPort = ntohs(clientaddr.sin_port);
printf("clinet IP is: %s, port is: %d\n", clientIP, clientPort);

// 5.获取客户端数据
char recvBuf[1024] = {0};
while (1) {
len = read(cfd, recvBuf, sizeof(recvBuf));
if (len == -1) {
perror("read");
exit(0);
} else if (len > 0) {
printf("recv client data: %s\n", recvBuf);
} else if (len == 0) {
printf("client close..\n");
break;
}
// 给客户端发送数据
char* sendBuf = "hello , i am server";
write(cfd, sendBuf, strlen(sendBuf));
}

// 关闭文件描述符
close(lfd);
close(cfd);
return 0;
}

TCP三次握手

TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP 地址、端口号等。 TCP 可以看成是一种字节流,它会处理 IP 层或以下的层的丢包、重复以及错误问题。在连接的建立过程 中,双方需要交换一些连接的参数。这些参数可以放在 TCP 头部。 TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用 四次挥手 来关闭一个连接。

  • 三次握手
    • 目的是保证双方互相之间建立了连接
  • TCP头部信息
    • image-20230909093153480

三次握手发生在客户端连接的时候,当客户端调用connect(),底层会通过TCP协议进行三次握手

  • 三次握手时序图

    image-20230909094819496

    第一次握手:

    ​ 1.客户端将SYN标志置为1

    ​ 2.生成一个随机的32位的序号,这个序号后边是可以携带数据(数据的大小)

    第二次握手

    ​ 1.服务器端接收客户端的连接:ACK = 1

    ​ 2.服务器回发一个确认信号ack = 客户端的seq + 数据长度 + SYN/FIN(占1B)

    ​ 3.服务器向客户端发起连接请求:SYN = 1

    ​ 4.服务器生成一个随机序号:seq = k

    第三次握手

    ​ 1.客户端应答服务器的连接请求:ACK = 1;

    ​ 2.客户端回复收到服务器端的数据:ack = 服务器的序号 + 数据长度 + SYN/FIN(占1B)

    • 根据TCP头部信息进行三次握手

      image-20230909095025536

    • 双方确认两者的接发数据功能没问题至少需要三次握手

  • 怎么确定发送的数据是完整的?

    为每个字节分配一个32位序号,seq是字节序第一个字节的序号

    ack为期望收到的下一个字节的序号(即上一次收到的字节序中最后一个字节的序号 + 1)

    • 搞清楚每一个信号的原因!

    image-20230909101348440

  • 怎么确定接收和发送顺序是一样的?

    滑动窗口

滑动窗口

滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的 拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包, 谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包 (称窗口尺寸)。 TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方一般不能再发送数据报。 滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构

  • 窗口理解为缓冲区的大小
    • 滑动窗口的大小会随着发送数据和接收数据而变化
    • 通信双方都有发送缓冲区和接收缓冲区
  • image-20230909104107465

发送方缓冲区

​ 白色格子:空闲的空间

​ 灰色格子:数据已经被发送出去了,但是还没有被接受

​ 粉色格子:还未发送的数据

接收方缓冲区

​ 白色格子:空闲的空间

​ 粉色格子:已经接受到的数据

image-20230909104422000

MSS:maximum segment size (一条数据的最大数据量)

win:滑动窗口大小

1.客户端给服务器发起连接,客户端的滑动窗口大小4096,一次发送的最大数据量为1460

2.服务器接受连接请求,告诉客户端服务器窗口大小6144,一次发送的最大数据量为1024

3.第三次握手

4-9.客户端连续给服务器发送了6k数据,每次发送1k

10.服务器给客户端发送数据,告诉客户端服务器已经接受了6k数据,存储在缓冲区中,剩余空间2k

11.服务器给客户端发送数据,告诉客户端服务器已经接受了6k数据,存储在缓冲区中,剩余空间4k

12.客户端给服务器发送1k数据

13.客户端主动请求断开连接,并且给服务器发送了1k的数据

14.服务器收到,回复ACK8194(同意断开连接请求(+1),告诉客户端已经接收到刚才发的2k数据),滑动窗口剩余2k

15-16.通知客户端滑动窗口大小

17.第三次挥手:服务器给客户端发送FIN,请求断开连接

18.第四次挥手:客户端同意断开连接,连接彻底断开

TCP四次挥手

四次挥手:在程序中当调用了close()会使用TCP协议进行四次挥手

客户端和服务端都可以主动发起断开连接,谁调用close()谁就发起

因为TCP在建立连接时,采用三次握手建立双向连接,在断开的时候也需要双向断开

image-20230909110059973

image-20230909105622736

多进程实现并发服务器

要实现TCP通信服务器并发处理任务,使用多线程或者多进程解决

思路:

​ 1.一个父进程,多个子进程

​ 2.父进程负责等待并接受客户端的连接

​ 3.子进程负责完成通信,接受一个客户端连接就创建一个子进程用于通信

  • 多进程通信的服务器端实现
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
101
102
103
104
105
106
107
108
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wait.h>

void recyleChild(int arg) {
while (1) {
int ret = waitpid(-1, NULL, WNOHANG); //-1 回收所有子进程 WNOHANG设置非阻塞
if (ret == -1) {
printf("所有的子进程回收\n");
break;
} else if (ret == 0) {
break;
} else {
printf("子进程%d被回收了\n", ret);
}
}
}

int main() {
// 注册信号捕捉
struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = recyleChild;

sigaction(SIGCHLD, &act, NULL);

// 创建套接字
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if (lfd == -1) {
perror("socket");
exit(0);
}

// bind
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = ntohs(9999);
saddr.sin_addr.s_addr = INADDR_ANY;

int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret == -1) {
perror("bind");
exit(0);
}

// listen
ret = listen(lfd, 128);
if (ret == -1) {
perror("listen");
exit(0);
}

while (1) {
// accept
struct sockaddr_in caddr;
int len = sizeof(caddr);

int cfd = accept(lfd, (struct sockaddr*)&caddr, &len);
if (cfd == -1) {
if (errno == EINTR) {
// 产生中断
continue;
}
perror("accept");
exit(0);
}

pid_t pid = fork();
if (pid == 0) {
// 子进程
// 获取客户端的信息
char clientIP[16] = {0};
inet_ntop(AF_INET, &caddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
unsigned short port;
port = ntohs(caddr.sin_port);
printf("clinet IP: %s, port: %x\n", clientIP, port);

// 读数据
char recvBuf[1024];
while (1) {
int readLen = read(cfd, recvBuf, sizeof(recvBuf));
if (readLen > 0) {
printf("recv client: %s\n", recvBuf);
} else if (readLen == 0) {
printf("client closed..\n");
break;
} else if (readLen == -1) {
perror("read");
exit(0);
}
// 回写数据
write(cfd, recvBuf, strlen(recvBuf) + 1);
}
close(cfd);
// 退出子进程
exit(0);
}
}
close(lfd);
return 0;
}

  • 多进程通信的客户端实现
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
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("socket");
exit(0);
}
// 2.连接服务器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, " 192.168.203.59", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);

int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if (ret == -1) {
perror("connect");
exit(0);
}

// 通信
char recvBuf[1024];
int i = 0;
while (1) {
// 给客户端发送数据
sprintf(recvBuf, "data: %d\n", i++);
write(fd, recvBuf, strlen(recvBuf) + 1);
sleep(1);
// 读数据
int len = read(fd, recvBuf, sizeof(recvBuf));
if (len == -1) {
perror("read");
exit(0);
} else if (len > 0) {
printf("recv server data: %s\n", recvBuf);
} else if (len == 0) {
printf("server close..\n");
break;
}
}

close(fd);
return 0;
}

多线程实现并发服务器

  • 客户端与多进程不变
  • 服务器端的实现
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

struct sockInfo {
int fd; // 通信的文件描述符
struct sockaddr_in addr;
pthread_t tid; // 线程号
};

struct sockInfo sockinfos[128];

void * working(void * arg) {
// 子线程和客户端通信 cfd 客户端的信息 线程号
// 获取客户端的信息
struct sockInfo * pinfo = (struct sockInfo *)arg;

char cliIp[16];
inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(pinfo->addr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

// 接收客户端发来的数据
char recvBuf[1024];
while(1) {
int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));

if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
}
close(pinfo->fd);
return NULL;
}

int main() {

// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}

struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;

// 绑定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}

// 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}

// 初始化数据
int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
for(int i = 0; i < max; i++) {
bzero(&sockinfos[i], sizeof(sockinfos[i]));
sockinfos[i].fd = -1;
sockinfos[i].tid = -1;
}

// 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
while(1) {

struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接受连接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

struct sockInfo * pinfo;
for(int i = 0; i < max; i++) {
// 从这个数组中找到一个可以用的sockInfo元素
if(sockinfos[i].fd == -1) {
pinfo = &sockinfos[i];
break;
}
if(i == max - 1) {
sleep(1);
i--;
}
}

pinfo->fd = cfd;
memcpy(&pinfo->addr, &cliaddr, len);

// 创建子线程
pthread_create(&pinfo->tid, NULL, working, pinfo);

pthread_detach(pinfo->tid);
}

close(lfd);
return 0;
}

TCP通信

状态转换发生在三次握手和四次挥手的时候

image-20230910122813061

  • 三次握手的状态转换

1.客户端调用connect发送SYN请求之后,变成SYN_SENT状态

2.服务器接受到请求连接报文,转换成SYN_RCVD状态,像客户端发送确认报文

3.客户端接收到确认报文,状态变为ESTABLISHED

  • 四次挥手的状态转换

1.客户端发送FIN断开连接,状态变为FIN_WAIT_1

2.服务器接收到断开连接请求,状态变为CLOSE_WAIT,并发送确认报文给客户端

3.客户端接收到确认报文,状态转换为FIN_WAIT2

4.服务器发送断开连接请求报文给客户端,状态变为LAST_ACK

5.客户端接受到报文,状态变为TIME_WAIT,并发送确认报文

四次挥手之后两边都变成close状态(既是起点,也是重点)

  • image-20230910124941349

    • 黑色线:异常(暂时忽略)
    • 绿色虚线:服务端
    • 红色实线:客户端
  • TIME_WAIT定时经过两倍的报文端寿命之后(2MSL),才会结束

    为什么呢?

  • 第二次挥手和第三次挥手之间可能间隔时间长,还可以发送数据

  • 确保数据安全性,确保收到所有数据

  • 确保通信另一段正确接受ACK

    • 比如服务端也可能没有收到ACK,需要客户端重新发送,也要求客户端不能立即断开
    • 被动房重传的不是ACK,是FIN

image-20230910130012761

半关闭和端口复用

  • 只要调用close之后就不能发送数据了
    • 四次挥手之后发送ack是协议的行为,不能发送带有数据的报文
    • 可以接受数据

当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2 状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发 送的数据,但是 A 已经不能再向 B 发送数据。

  • shutdown函数
1
2
3
4
5
6
7
8
9
10
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd: 需要关闭的socket的描述符
how: 允许为shutdown操作选择以下几种方式:
SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发
出写操作。
SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以
SHUT_WR。

使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。

  • 如果有多进程共享一个套接字,close每调用一次,计数-1,直到计数为0。也就是所有进程都调用了close,套接字将被释放
  • 在多进程中如果一个进程调用了shutdown(sfd,SHUT_RDWR)之后,其他的进程将无法进行通信,但如果一个进程close(sfd)不会影响到其他进程

  • 端口复用
  • 查看网络信息相关的命令

net-stat

​ -a 所有的socket

​ -p 显示正在使用socket的程序的名称

​ -n 直接使用IP地址,而不通过域名服务器

  • setsockopt()

    ​ 解决问题:

    • 1.放置服务器重启时之前绑定的端口还未释放
    • 2.程序突然退出而系统端口还未释放
  • 不仅仅设置端口复用,设置套接字的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
-sockfd:要操作的文件描述符
-level:级别,SOL_SOCKET(端口复用级别)
-optname:选项的名称
SO_REUSEADDR
SO_REUSEPORT
-optval:端口复用的值,是一个整形
1:端口可复用
2:不可复用
-optlen:optval参数的大小
端口复用设置的时机:服务器绑定端口之前

IO多路复用

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

  • 传统的I/O指的是程序和内存之间的输入输出

几种IO模型

  • 阻塞等待BIO
    • 好处:不占用CPU宝贵的时间片
    • 缺点:同一时刻只能处理一个操作,效率低
  • 解决:使用多线程或者多进程方式解决
    • 优点:解决效率低的问题
    • 缺点:
      • 线程或者进程会消耗资源
      • 线程或进程调度消耗CPU资源

  • 非阻塞,忙轮询NIO
    • 优点:提高了程序的执行效率
    • 缺点:需要占用更多的CPU和系统资源
      • 每次循环都会一次询问,时间复杂度N(O)

  • 多路转接计数

    • select

    image-20230910161139291

    • 底层是二进制数组,只知道有多少,不知道具体是哪几个文件描述符
  • epoll

    • 同样也是委托内核,不仅能检测有几个,还能找出具体的文件描述符

    image-20230910161327082

selectAPI

  • 主旨思想

1.首先构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。

2.调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回

​ 该函数是阻塞的,当检测到这些描述符中的一个或者多个进行I/O操作时才返回

函数对文件描述符的检测的操作是由内核完成的

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
// 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:要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
一般检测读操作
对应的是对方发送过来的数据,因为读是被动的接受数据,检测的是读缓冲区
是一个传入传出参数,内核会修改fd_set中的值并返回,这也是为什么用指针
-writefds:要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
检测的是写缓冲区还有多少空余缓存,一般不使用
-exceptfds:检测发生异常的文件描述符的集合,一般不使用
-timeout:一个结构体,设置的超时时间
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
值为NULL:永久阻塞直到检测到了文件描述符的变化、
tv_sec == 0 && tv_usec == 0:不阻塞
tv_sec > 0 && tv_usec > 0:阻塞对应的时间
返回值:
失败返回-1
n(>0):检测到的集合中有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-20230910195454961

  • select的使用
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
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;

// 绑定
int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret == -1) {
perror("bind");
exit(0);
}
// 监听
listen(lfd, 8);

// 创建一个fd_set集合,存放的是需要检测的文件描述符
// 让tmp集合交给内核操作,rdset只能通过函数调用修改
fd_set rdset, tmp;
FD_ZERO(&rdset);
int maxfd = lfd;

// 添加
FD_SET(lfd, &rdset);
while (1) {
tmp = rdset;
// 调用select系统函数,让内核帮忙检测那些文件描述符有数据
ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
if (ret == -1) {
perror("select");
exit(0);
} else if (ret == 0) {
continue;
} else if (ret > 0) {
// 检测到了有文件描述符对应的缓冲区的数据发生了改变
if (FD_ISSET(lfd, &tmp)) {
// 表示有新的客户端连接进来了
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
// 将新的文件描述符加入到集合中
FD_SET(cfd, &rdset);
// 更新最大的文件描述符
maxfd = maxfd > cfd ? maxfd : cfd;
}
for (int i = lfd + 1; i <= maxfd; i++) {
// lfd一定是在最前面的,所有从后面开始监听
if (FD_ISSET(i, &tmp)) {
// i文件描述符对应的客户端发来了数据
// 通信
char buf[1024] = {0};
int readlen = read(i, buf, sizeof(buf));
if (readlen == -1) {
perror("read");
exit(0);
} else if (readlen == 0) {
printf("client closed...\n");
FD_CLR(i, &rdset);
close(i);
} else {
printf("read buf: %s\n", buf);
write(i, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);

return 0;
}

  • 总结
    • 缺点
      • 每次调用select要将fds从用户态拷贝到内核态,资源消耗大
      • 每次调用select都需要在内核遍历传递进来的所有fd,开销也很大
      • select支持的文件描述符数量小,默认是1024
      • fds集合不能重用,每次都需要重置。一般需要定义另外一个来使用,需要额外开闭数组

pollAPI

image-20230911090510865

  • poll服务器代码实现
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
101
102
103
104
105
106
107
108
109
110
/*
#include <poll.h>

struct pollfd {
int fd; file descriptor
short events; requested events
short revents; returned events
};
参数
-fds: 是一个struct pollfd结构体数组,这是一个需要检测的文件描述符的集合
-nfds: 这个是第一个参数数组中最后一个有效元素的下标 + 1、
-timeout: 阻塞时长
0: 不阻塞
-1: 阻塞,当检测到需要检测的文件描述符有变化时解除阻塞
> 0: 阻塞时长
返回值
-1 表示失败
>0(n) 表示成功,集合中有n个文件描述符发生变化
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

*/

#include <arpa/inet.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;

// 绑定
int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret == -1) {
perror("bind");
exit(0);
}
// 监听
listen(lfd, 8);

// 初始化检测文件描述符数组
struct pollfd fds[1024];
for (int i = 0; i < 1024; i++) {
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = lfd;
int nfds = 0;

while (1) {
// 调用select系统函数,让内核帮忙检测那些文件描述符有数据
int ret = poll(fds, nfds + 1, -1);
if (ret == -1) {
perror("select");
exit(0);
} else if (ret == 0) {
continue;
} else if (ret > 0) {
// 检测到了有文件描述符对应的缓冲区的数据发生了改变
if (fds[0].revents & POLLIN) {
// 表示有新的客户端连接进来了
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
// 将新的文件描述符加入到集合中
for (int i = 1; i < 1024; i++) {
if (fds[i].fd == -1) {
fds[i].fd = cfd;
fds[i].events = POLLIN;
break;
}
}
// 更新最大的文件描述符
nfds = nfds > cfd ? nfds : cfd;
}
for (int i = 1; i <= nfds; i++) {
// lfd一定是在最前面的,所有从后面开始监听
if (fds[i].revents & POLLIN) {
// i文件描述符对应的客户端发来了数据
// 通信
char buf[1024] = {0};
int readlen = read(fds[i].fd, buf, sizeof(buf));
if (readlen == -1) {
perror("read");
exit(0);
} else if (readlen == 0) {
printf("client: %d closed...\n", fds[i].fd);
close(fds[i].fd);
fds[i].fd = -1;
} else {
printf("read buf: %s\n", buf);
write(fds[i].fd, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);

return 0;
}
  • 总结
    • poll解决了select支持文件描述符数量小和fds不能重复使用,每次都需要重置的缺点

epollAPI

  • epoll_create创建的实例在内核区,eventpoll,是一个结构体数据。返回值是文件描述符,利用它对内核中的结构体进行操作(并不是直接操作,调用API)

    • 没有用户态到内核态的开销,提高了效率
    • 底层是红黑树,遍历效率高,之前两个API是线性数组
  • epoll_wait会向用户区返回哪些文件描述符发生改变

    • 这里的交换只有一次,速度很快。而且会返回具体哪些fd发生改变
  • rb_root->红黑树 list_head -> 链表

image-20230911094439422

  • 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
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/*
#include <sys/epoll.h>
功能: 创建一个新的epoll实例,在内核中创建一个数据,这个数据
有两个重要数据:一个是需要检测的文件描述符的信息(红黑树)
另一个是就绪列表,存放检测数据发送改变的文件描述符信息(双向链表)
参数
-size 改版之后无意义,随便填一个大于0的书
返回值
成功返回>0:文件描述符,操作epoll实例
失败返回-1
- int epoll_create(int size);

功能: 对epoll实例进行管理,添加文件描述符信息,删除修改信息
参数
-epfd:epoll实例对应的文件描述符
-op:要进行什么操作
EPOLL_CTL_ADD:添加
EPOLL_CTL_MOD:修改
EPOLL_CTL_DEL:删除
-fd:要检测的文件描述符
-event:检测文件描述符什么事情
struct epoll_event {
uint32_t events; Epoll events
epoll_data_t data; User data variable
};
常见的epoll检测事件
EPOLLIN
EPOLLOUT
EPOLLERR
typedef union epoll_data { //联合体,只有一个会存在
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
};
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *_Nullable event);

功能:检测函数
参数
-epfd:epoll实例对应的文件描述符
-events:传出参数,保存了发生变化的文件描述符的信息
-maxevents:第二个结构体参数的大小
-timeout:阻塞事件
0表示不阻塞
-1表示阻塞,直到检测到fd数据发生变化时解除阻塞
> 0 阻塞时长(mm)
返回值
成功返回>0的数,表示变化的文件描述符的个数
失败返回 -1
- int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
*/
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <unistd.h>

int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;

// 绑定
int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret == -1) {
perror("bind");
exit(0);
}
// 监听
listen(lfd, 8);

// 用epoll_create() 创建一个epoll实例
int epfd = epoll_create(1);
// 将监听的文件描述符的监测信息加到epoll的实例当中(内核中)
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

// 保存内核检测之后返回的文件描述符的信息
struct epoll_event epevs[1024];
while (1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if (ret == -1) {
perror("epoll_wait");
exit(0);
}
// ret是检测到有多少个文件描述符发生改变的值
printf("ret = %d\n", ret);
for (int i = 0; i < ret; i++) {
if (epevs[i].data.fd == lfd) {
// 监听的文件描述符有数据到达,即有新的客户端连接
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
// 封装cfd文件描述符的信息到内核中,进行监听
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT){
printf("检测到写\n");
continue;
}
// 有数据到达,需要通信
char buf[1024] = {0};
int len = read(epevs[i].data.fd, buf, sizeof(buf));
if (len == -1) {
perror("read");
exit(0);
} else if (len == 0) {
printf("client %d closed...\n", epevs[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, epevs[i].data.fd, NULL);
close(epevs[i].data.fd);
} else if (len > 0) {
printf("read buf: %s\n", buf);
write(epevs[i].data.fd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}

epoll的工作模式

  • LT模式(水平触发)
  • 假设委托内核检测读事件
    • ->检测fd的读缓冲区
      • 若读缓冲区有数据->epoll检测到了会通知用户
        • 若用户不读数据,数据一直在缓冲区,epoll下一次会继续通知用户
        • 若用户只读了一部分数据,epoll也会通知
        • 若缓冲区数据读完,epoll不通知

image-20230911154558253

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

作。如果你不作任何操作,内核还是会继续通知你的

​ - 在一个文件描述符中,读缓冲区有数据,你只读了一部分,epoll下一次还是会通知你。除非读完

  • ET模式(边沿触发)
  • 假设委托内核检测读事件
    • ->检测fd 的读缓冲区
      • 若读缓冲区有数据->epoll检测到了会通知用户
        • 若用户不读数据,数据一直在缓冲区中,epoll下次检测时不通知
        • 若用户读了一部分数据,epoll也不会通知
        • 若数据读完,也不会通知

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

​ -当fd缓冲区中有数据了,epoll会且仅会通知你一次,若数据未读完,下一次也不会再通知,只有当你将缓存中的数据完全读完了,下一次有数据时epoll才会通知

​ -效率比水平触发高,但是需要循环读数据将数据全部读出来。但是read读不到数据可能会阻塞,所以需要使用no-block socket

UDP通信

image-20230911205705304

  • UDP不需要多线程或者多进程就可以实现多个客户端进行通信
  • 服务器端代码
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
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {
// 创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if (fd == 0) {
perror("socket");
exit(0);
}
// bind
struct sockaddr_in addr;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(9999);
addr.sin_family = AF_INET;
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
perror("bind");
exit(0);
}

// 通信
while (1) {
// 接收数据
char ipbuf[18];
char recvbuf[1024] = {0};
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int num = recvfrom(fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr*)&cliaddr, &len);
if (num == -1) {
perror("revcfrom");
exit(0);
}
printf("client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
ntohs(cliaddr.sin_port));
printf("client say: %s\n", recvbuf);

// 发送数据
sendto(fd, recvbuf, strlen(recvbuf) + 1, 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
}
return 0;
}
  • 客户端代码
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
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {
// 创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if (fd == 0) {
perror("socket");
exit(0);
}
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr);

int num = 1;
// 通信
while (1) {
// 接收数据
char ipbuf[18];
char sendbuf[1024] = {0};
sprintf(sendbuf, "hello , i am clent %d\n", num++);

// 发送数据
sendto(fd, sendbuf, strlen(sendbuf) + 1, 0, (struct sockaddr*)&saddr, sizeof(saddr));
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int num = recvfrom(fd, sendbuf, sizeof(sendbuf), 0, NULL, NULL);

printf("server say: %s\n", sendbuf);
sleep(1);
}
close(fd);
return 0;
}

广播

向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息。每个广播消息都有一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全为1,255为广播IP

1.只能在局域网中使用

2.客户端需要绑定服务器广播使用的端口,才可以接受到广播消息

组播(多播)

  • 多播地址标识一组IP接口。单播和广播是寻址方案的两个极端(要么单个要么全部),多播则意在两者之间提供一种折中的方案。
  • 多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应的多播会话应用系统的主机上的接口接收
  • 广播一般局限于局域网内使用,而组播则既可以用于局域网,也可以跨广域网使用
    • 组播既可以用于局域网,也可以用于广域网
    • 客户端需要加入组播组,才能接收到组播的数据
  • image-20230913201916158
  • 组播地址(224.0.0.0到239.255.255.255)
    • image-20230913202027081

本地套接字通信

作用:本地的进程间通信

​ 有关系的进程间的通信

​ 没有关系的进程间的通信

  • 本地套接字通信流程

    服务器端(一般按TCP流程通信)

1.创建监听的套接字

​ int lfd = socket(AF_LOCAL / AF_LOCAL, SOCK_STREM, 0);

2.监听的套接字绑定本地的套接字文件 -> server端

​ struct sockaddr_un addr;

​ //绑定成功之后,指定的sum_path中的套接字文件会自动生成

​ bind(lfd, addr, len);

3.监听

​ listen(lfd, 100);

4.等待并接受连接请求

​ struct socket_un cliaddr;

​ int cfd = accept(lfd, &liaddr, len);

5.通信

​ 接收数据:read/recv

​ 发送数据:write/send

6.关闭连接

​ close(lfd);

​ closee(cfd);

  • 客户端

1.创建通信的套接字

​ int fd = socket(AF_UNIX/ AF_LOCAL, SOCK_STREAM, 0);

2.监听的套接字绑定本地的IP和端口

​ struct sockaddr_un addr;

​ //绑定成功之后, 指定的sun_path中的套接字文件会自动生成

​ bind(fd, &addr, len);

3.连接服务器

​ struct sockaddr_un server_addr;

​ connect(fd, &server_addr, sizof(server_addr));

4.通信

​ 接收数据:read/recv

​ 发送数据:write/send

5.关闭连接

​ close();

Web服务器项目

阻塞/非阻塞、同步/异步(网络IO)

典型的一次IO的两个阶段:数据就绪和数据读写

  • 数据就绪:根据系统IO操作的就绪状态

    • 阻塞
      • 会将线程挂起,挂起的线程不会占用CPU资源
    • 非阻塞
      • 若没有数据,不会挂起,立即返回函数值

    image-20230917103911079

  • 数据读写:根据应用程序和内核的交互方式

    • 同步:缓冲区的内容自己搬到buf中,读取完之后才返回。这个过程阻塞,花的自己时间,编程简单
    • 异步:效率高,编程复杂,缓冲区的内容操作系统搬到buf中

    image-20230917104652319

在处理IO的时候,阻塞和非阻塞都是同步IO,只用使用了特殊的API才是异步IO

一个典型的网络IO接口调用,分为两个阶段,分别是“数据就绪”和“数据读写”,数据就绪阶段分为阻塞和非阻塞,表现的结构就是,阻塞当前线程或者是直接返回

同步表示A向B请求调用一个网络IO接口时(或API),数据的读写都是由请求方A自己来完成的(不管是阻塞还是非阻塞);异步表示A向B请求调用一个网络IO,向B传入请求的事件以及事件发生的通知方式,A就可以处理其他逻辑了,当B监听到事件处理完成后,会用事先预定好的通知方式通知A处理结果

  • 同步阻塞
  • 同步非阻塞
  • 异步阻塞
  • 异步非阻塞

image-20230917113854019

UNIX、Linux上的五种IO模型

阻塞blocking

  • 调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回值,必须等待这个函数返回才能进行下一步动作

    image-20230917122034693

  • 该模型效率太低,不友好

非阻塞non-bkocking(NIO)

  • 非阻塞等待,每隔一段时间就去检测IO事件是否就绪,没有就绪就去做其他事情。非阻塞IO执行系统调用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据errno区分两种情况,对于accept、revc和send,时间未发生时,errno通常被设置成EAGAIN

    image-20230917123018739

IO复用IO multiplexing

  • Linux用select、pol、epollAPI实现IO复用模型,这些函数也会使进程阻塞,但是和阻塞IO所不同的是这些函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。直到有数据可读或可写时,才真正调用IO操作函数
    • 一次检测多个客户事件

image-20230917123912478

信号驱动 signal-driven

  • Linux用套接口进行信号驱动,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进程收到SIGIO信号,然后处理IO事件

image-20230917124432371

  • 内核在第一个阶段是异步的,在第二个阶段是同步的
  • 与非阻塞IO区别在于它提供了消息通知机制,不需要用户进程不断的轮训检查,减少了系统API的调用次数,提高了效率

异步 asynchoronous

  • Linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序

    • 使用较少

    image-20230917124906786

Web Server 和Http

image-20230917153603724

  • Http是应用层协议

    image-20230917154514268

    image-20230917154635801

  • 过程

    image-20230917155537962

  • http协议是基于TCP/IP协议之上的应用层协议,基于请求-响应的模式。HTTP协议规定,请求从客户端发出,最后服务器端响应请求并返回。

HTTP格式

  • 请求报文格式

    image-20230917160108259

    image-20230917160519649

  • 响应报文请求格式

    image-20230917160137415

服务器编程基本框架

虽然服务器程序种类繁多,但基本框架都一样,不同指出在于逻辑处理

image-20230917204550350

  • IO处理单元是服务器管理客户连接的模块。它通常要完成以下工作:等待并接受新的客户连接,接受客户数据,将服务器响应数据返回给客户端。但数据的收发不一定在IO处理单元中执行,也可能在逻辑单元中执行,具体在何处执行取决于事件处理模式
  • 逻辑单元通常是一个进程或线程,它分析并处理客户数据,然后将结果传递给IO处理单元或直接发送给客户端(取决于事件处理模式)。服务器通常拥有多个逻辑单元,以实现对多个客户的任务的并发处理
  • 网络存储单元可以是数据库、缓存和文件,但不是必须的
  • 请求队列各单元之间的通信方式的抽象。IO处理单元接收到客户请求时,需要以某种方式通知一个逻辑单元来处理该请求。同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处理竞态条件,请求队列通常被实现为池的一部分

两种高效的事件处理模式

(7 封私信 / 80 条消息) 如何深刻理解Reactor和Proactor? - 知乎 (zhihu.com)

服务器程序通常需要处理三类事件:IO事件信号定时事件

两种高效的事件处理模式:Reactor和Proactor,同步IO模型通常用于实现Reactor模式,异步IO模型通常用于实现Proactor模式

Reactor模式
  • 要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话立即将该事件通知工作线程(逻辑单元,子线程),将socket可读可写事件放入请求队列,交给工作线程处理。除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。

  • 是非阻塞同步网络模式

  • 使用同步I/O(以epoll_wait为例)实现Reactor模式的工作流程是:

    1. 主线程往epoll内核事件表中注册socket上的读就绪事件
    2. 主线程调用epoll_wait等待socket上有数据可读
    3. 当socket上有数据可读时,epoll_wait通知主线程。主线程则将socket可读事件放入请求队列
    4. 睡眠在请求队列的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件
    5. 当主线程调用epoll_wait等待socket可写
    6. 当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列。
    7. 睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户的请求结果

image-20230917212906493

Reactor 模式主要由Reactor和处理资源池这两个核心部分组成。

  • Reactor 负责监听和分发事件,事件类型包含连接事件、读写事件
  • 处理资源池负责处理事件,如 read -> 业务逻辑 -> send;

Reactor 模式是灵活多变的,可以应对不同的业务场景,灵活在于:

  • Reactor 的数量可以只有一个,也可以有多个;
  • 处理资源池可以是单个进程 / 线程,也可以是多个进程 /线程;

Reactor是非阻塞同步网络。事件由操作系统通知应用进程,进程处理

Proactor模式
  • 工作线程仅仅负责业务逻辑。不负责读写

image-20230917212937097

  • 工作流程

    image-20230917213358873

  • 同步IO工作流程

image-20230917220658798

image-20230917220754510

因此,Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。这里的「事件」就是有新连接、有数据可读、有数据可写的这些 I/O 事件这里的「处理」包含从驱动读取到内核以及从内核读取到用户空间。

举个实际生活中的例子,Reactor 模式就是快递员在楼下,给你打电话告诉你快递到你家小区了,你需要自己下楼来拿快递。而在 Proactor 模式下,快递员直接将快递送到你家门口,然后通知你

Proactor是异步网络模式,感知的是已经完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。

事件由操作系统处理,处理完成后通知应用进程


线程池

  • 线程池是由服务器预先创建的一组子线程,线程池中的线程号数量应该和CPU数量差不多。线程池中所有子线程都运行着相同的代码。当有新的任务到来时,主线程将通过某种方式选择线程池中的某一个子线程来为之服务。

    • 比动态创建子线程开销小得多
  • 如何选择子线程?

    • 随机算法、轮流选取(Round Robinsuanfa
    • 主线程和所有子线程通过一个共享队列来同步,子线程都睡眠在该工作队列上,当有新的任务到来时,主线程将任务添加到工作队列中。这将唤醒正在等待任务的子线程,只有一个子线程将获得新任务的接管权,它可以从工作队列中取出任务并执行,其他子线程将继续睡眠在工作队列上
  • 一般模型

    image-20230918213759953

  • 几个概念

  • 空间换时间:浪费服务器的硬件资源,换取运行效率

  • 池是一组资源的集合,这组资源在服务器启动之初就被完全创建好并初始化,这称为静态资源

  • 当服务器进入正式运行阶段,开始处理客户请求的时候,如果它需要相关的资源,可以直接从池中获取,无需动态分配

  • 当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用释放资源


文章作者: jingxiaoyang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 jingxiaoyang !
评论
  目录