大纲

IO就是: input、output(输入输出)

标准IO

文件IO

LinuxIO模型

进程:process

进程基础

进程间通信:无名管道(pipe)、有名管道(fifo)、信号(signal)、共享内存(shared memory)、信号灯集(semphore set)、消息队列(message queue)

线程(thread)、同步、互斥、条件变量

学习特点:逻辑性没那么强,但是内容多。

标准IO

1.什么是标准IO

1.1 概念

标准IO:是在C库中定义的一组专门用于输入输出的函数。

1.2 特点

1.2.1通过缓冲机制减少系统调用,提高效率。

系统调用:内核向上提供的一组接口。

例如:从硬盘1KB文件,每次读1B

1.2.2围绕流进行操作,流用FILE*来描述。

1)vi -t 查找名称

输入前面序号,回车。

2)继续追踪:

将光标定位到要追踪的内容,ctrl+]

回退:ctrl+t

3)跳转到上次位置:ctrl+o

跳转到下次位置:ctrl+i

vscode索引:

跳转到定义:ctrl 左键

前后跳转:

ctrl alt -

ctrl shift -

  1. 标准IO默认打开三个流,stdin(标准输入)、stdout(标准输出)、stderr(标准错误)

2.缓存区

2.1全缓存:和文件相关

刷新全缓存:

  • 程序正常退出
  • 缓存区满
  • 强制刷新: fflush(NULL)

2.2行缓存:和终端相关

刷新标准输出缓冲的条件:

  • 程序正常退出
  • 缓存区满
  • 强制刷新: fflush(NULL)
  • \n
    // printf("hello world\n"); //'\n'不光是换行,还是刷新标准输出缓存的条件
    printf("hello world");
    fflush(NULL); //强制刷新缓存
    while(1);  //为了让程序不要结束
    return 0; //程序正常退出

2.3不缓存:没有缓冲,标准错误

利用结构体指针stdout

综上:当我们每次要打印数据时,并不是直接将数据发送给标准输出设备,也就是直接发送给显示器展示。而是先将要打印的数据放到缓存区,当达到刷新缓存区的条件的时候,才会把数据传输到标准输出设备中,也就是显示器中进行输出了。

3.函数接口

3.1 打开文件 fopen

man 3 fopen
FILE *fopen(const char *path, const char *mode);
功能:打开文件
参数:
    path:打开的文件路径
    mode:打开的方式
        r:只读,当文件不存在时错误,文件流定位到文件开头
        r+:可读可写,当文件不存在时报错,文件流定位到文件开头
        w:只写,文件不存在创建,存在则清空
        w+:可读可写,文件不存在创建,存在则清空
        a:追加(在末尾写),文件不存在创建,存在追加,文件流定位到文件末尾
        a+:读和追加,文件不存在创建,存在追加,读文件流定位到文件开头,写文件流定位到文件末尾
注:当a+的方式打开文件时,写只能在末尾进行追加,定位操作是无法改变写的位置,但是可以改变读的位置

返回值:
成功:文件流
失败:NULL,并且会设置错误码

补充perror:

void perror(const char *s);
功能:根据errno值打印对应的错误信息
参数:
	  s:提示语句
返回值:空

补充:perror( )用来将上一个函数发生错误的原因输出到标准设备(stderr) 。
参数 s 所指的字符串会先打印出, 后面再加上错误原因字符串。
此错误原因依照全局变量errno 的值来决定要输出的字符串。 
在库函数中有个errno变量,每个errno值对应着以字符串表示的错误类型。
当你调用"某些"函数出错时,该函数已经重新设置了errno的值。
perror函数只是将你设置的一些信息和现在的errno所对应的错误一起输出。

3.2 关闭文件 fclose

int fclose(FILE* stream);
功能:关闭文件
参数:stream:文件
#include <stdio.h>

int main(int argc, char const *argv[])
{
    // 打开文件
    FILE *fp;
    // fp = fopen("a.c", "r"); // r:只读,不存在则报错
    fp = fopen("b.c", "w"); // w:只写,不存在创建,存在则清空
    if (NULL == fp)
    {
        perror("fopen err");
        return -1;
    }
    printf("fopen ok\n");

    // 关闭文件
    fclose(fp);

    return 0;
}

3.3 文件读写操作:fgets、fputs、fread、fwrite

3.3.1 每次读写一个字符串: fgets和fputs
char * fgets(char *s,  int size,  FILE * stream);
功能:从文件中每次读取一行字符串
参数:   s:存放字符串的地址
         size:期望一次读取的字符个数
         stream:文件流
返回值:成功:s的地址
       失败或读到文件末尾:NULL
特性: 每次实际读取的字符个数为size-1个,会在末尾自动添加\0
       每次读一行,遇到\n或者到达文件末尾后不再继续读下一行
       并把它存储在s所指向的字符串内。

int  fputs(const char *s,  FILE * stream);
功能:向文件中写一个字符串
参数:s:要写的内容
     stream:文件流
返回值:成功:非负整数
       失败:EOF

1.针对终端

#include <stdio.h>

int main(int argc, char const *argv[])
{
    char buf[32] = "666666666666666";
    fgets(buf, sizeof(buf), stdin); // 会把换行也读入
    printf("%s", buf);              // 此时buf中内容:hello\n\066666666

    // for (int i = 0; i < 32; i++) //遍历数组验证
    //     printf("%d: %c\n", i, buf[i]);
    fputs(buf, stdout);
    fputs("777", stdout);
    return 0;
}

注意:fgets一定会留一个位置给'\0'

2.针对文件

3.3.2 二进制读写fread和fwrite

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:从文件流读取多个元素(将二进制数据从文件读出)
参数:  ptr :是一个指针,是存放数据的存储空间的起始地址,用来存放读取元素
       size :元素大小  sizeof(元素数据类型)
       nmemb :读取元素的个数
       stream :要读取的文件流
返回值:成功:读取的元素的个数
       读到文件尾或失败: 0

size_t fwrite(const void *ptr, size_t size, size_t nmemb,
              FILE *stream);
功能:将二进制数据写入文件
参数: ptr :是一个指针,保存要输出数据的空间的地址。
     size :要写入的字节数 sizeof(数据类型)
     nmemb : 要进行写入元素的个数
      stream: 目标文件流指针
返回值:成功:写的元素个数
              失败 :-1
#include <stdio.h>

int main(int argc, char const *argv[])
{
    FILE *fp = fopen("test.txt", "r+");
    if (NULL == fp)
    {
        perror("fopen err");
        return -1;
    }
    printf("fopen ok\n");

    float arr[3] = {1.2, 3.4, 5.6};
    fwrite(arr, sizeof(float), 3, fp);

    //因为上一步写完之后fp里面的位置到末尾了所以再度的话就读不出来东西了
    //所以可以将位置定位到文件的开头
    rewind(fp); //将位置定位到文件开头

    float data[3]={0};
    fread(data,sizeof(float),3,fp);
    printf("%f %f %f\n",data[0],data[1],data[2]);

    fclose(fp);

    return 0;
}

3.4 文件定位操作

void rewind(FILE *stream);
功能:将文件位置指针定位到起始位置

int fseek(FILE *stream, long offset, int whence);
功能:文件的定位操作
参数:stream:文件流
     offset:偏移量:正数表示向后文件尾部偏移,负数表示向文件开头偏移
     whence:相对位置:
           SEEK_SET:相对于文件开头
           SEEK_CUR:相对于文件当前位置
           SEEK_END:相对于文件末尾
返回值:成功:0
       失败:-1  
补充:其中SEEK_SET,SEEK_CURSEEK_END和依次为012.
注:当打开文件的方式为a或a+时,fseek不起作用   
例子:
把fp指针移动到离文件开头100字节处:fseek(fp,100,SEEK_SET);
把fp指针移动到离文件当前位置100字节处:fseek(fp,100,SEEK_CUR);
把fp指针退回到离文件结尾100字节处: fseek(fp,-100,SEEK_END);

long ftell(FILE *stream);
功能:获取当前的文件位置
参数:要检测的文件流
返回值:
成功:当前的文件位置,出错:-1

#include <stdio.h>

int main(int argc, char const *argv[])
{
    FILE *fp = fopen("test.txt", "r+");
    if (NULL == fp)
    {
        perror("fopen err");
        return -1;
    }
    printf("fopen ok\n");

    // 相对于文件开头往后定位10个字节
    fseek(fp, 10, 0);
    fputs("a", fp);

    // 相对于当前位置往后定位5个字节
    fseek(fp, 5, 1);
    fputs("hello", fp);

    // 相对于末尾位置往前定位1个字节
    fseek(fp, -1, 2);
    fputs("b", fp);

    // 获取当前文件位置
    printf("%ld\n", ftell(fp));

    fclose(fp);

    return 0;
}

总结:为什么用标准IO?

  1. 因为读写文件通常是大量的数据(相对于底层驱动的系统调用所实现的数据操作单位),这时,使用库函数可以大大减少系统调用的次数。
  2. 为了保证可移植性

关于缓存区: 库函数的缓冲区对于库函数,如果标准输出连到终端设备(直接输出到屏幕),则它是行缓冲的(遇到回车换行符或者是缓冲区满了才输出);否则(输出到文件)是全缓冲的(缓冲区填满或者是程序运行结束了才输出)。程序运行结束时,会刷新所有的缓冲区。

文件IO

1.什么是文件IO

1.1 概念

又称系统IO,是系统调用,是操作系统提供的函数接口。

posix中定义的一组专门用于输入输出的函数。

1.2 特点

  1. 没有缓冲机制,每次都会引起系统调用。
  2. 围绕着文件描述符进行操作,非负整数(>=0),依次分配。
  3. 文件IO默认打开三个文件描述符,分别是0(标准输入)、1(标准输出)、2(标准错误)。
  4. 操作除了目录文件(d)以外的任意类型文件(b c - l s p)
  5. 可移植性相对较弱

1.3 操作

打开文件:open

关闭文件: close

读写操作: read、write

定位操作: lseek

2.函数接口

r:O_RDONLY

w:O_WRONLY | O_CREAT | O_TRUNC

2.1

int open(const char *pathname, int flags);
功能:打开文件
参数:pathname:文件路径名
     flags:打开文件的方式
            O_RDONLY:只读
            O_WRONLY:只写
            O_RDWR:可读可写
            O_CREAT:不存在创建
            O_TRUNC:存在清空
            O_APPEND:追加   
返回值:成功:文件描述符
       失败:-1

当第二个参数中有O_CREAT选项时,需要给open函数传递第三个参数,指定创建文件的权限 
int open(const char *pathname, int flags, mode_t mode);
最后权限:mode & (~umask)
例如:指定权限为0666(八进制)
最终权限为: 0666 & (~0002) = 0666 & 0775 = 0664

思考:文件IO和标准IO的打开方式的对应关系

标准IO

文件IO

r

O_RDONLY

只读

r+

O_RDWR

可读可写

w

O_WRONLY|O_CREAT|O_TRUNC,0666

可写,不存在创建,存在则清空

w+

O_RDWR|O_CREAT|O_TRUNC,0666

可读可写,不存在创建,存在则清空

a

O_WRONLY|O_CREAT|O_APPEND,0666

可写,不能存在创建,存在追加

a+

O_RDWR|O_CREAT|O_APPEND,0666

可读可写,不能存在创建,存在追加

注意:有O_CREAT要加权限

2.2 关闭文件

int close(int fd);
功能:关闭文件
参数:fd:文件描述符
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    // 打开文件
    int fd;
    // fd = open("a.c", O_RDONLY); //r
    fd = open("test.c", O_WRONLY | O_CREAT | O_TRUNC, 0666); // w
    if (fd < 0)
    {
        perror("open err");
        return -1;
    }
    printf("fd: %d\n", fd);

    // 关闭文件
    close(fd);

    return 0;
}

2.3 读写文件read/write

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
功能:从一个已打开的可读文件中读取数据
参数: fd  文件描述符
      buf  存放位置
      count  期望的个数
返回值:成功:实际读到的个数(小于期望值说明实际没这么多)
       返回0:表示读到文件结尾
       返回-1:表示出错,并设置errno号
       
ssize_t write(int fd, const void *buf, size_t count);
功能:向指定文件描述符中,写入 count个字节的数据。
参数:fd   文件描述符
      buf   要写的内容
      count  期望写入字节数
返回值:成功:实际写入数据的个数
              失败  : -1
//返回值小于期望值是错误行为,可能磁盘满了无法再写。

fgets --> NULL末尾或失败

fread --> 0末尾或失败

read -->0末尾 -1失败

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

int main(int argc, char const *argv[])
{
    // 打开文件
    int fd;
    fd = open("a.c", O_RDWR); // r+
    if (fd < 0)
    {
        perror("open err");
        return -1;
    }
    printf("fd: %d\n", fd);

    char buf[32] = "";
    read(fd, buf, 32);
    printf("buf:%s\n", buf);

    write(fd, "world", 5);
    
    // 关闭文件
    close(fd);

    return 0;
}

2.4 文件定位操作 lseek

off_t lseek(int fd, off_t offset, int whence);
功能:设定文件的偏移位置
参数:fd:文件描述符
    offset: 偏移量  
        正数:向文件结尾位置移动
        负数:向文件开始位置
    whence: 相对位置
        SEEK_SET   开始位置
        SEEK_CUR   当前位置
        SEEK_END   结尾位置
补充:和fseek一样其中SEEK_SET,SEEK_CUR和SEEK_END和依次为0,1和2.

返回值:成功:文件的当前位置
        失败:-1
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    // 打开文件
    char buf[32] = "";
    int fd;
    fd = open("a.c", O_RDWR); // r+
    if (fd < 0)
    {
        perror("open err");
        return -1;
    }
    printf("fd: %d\n", fd);

    lseek(fd, 10, 2);
    write(fd, "k", 1);

    off_t len = lseek(fd, 0, 2);
    printf("%ld\n", len);  //利用定位到结尾求出文件长度

    // 关闭文件
    close(fd);

    return 0;
}

标准IO和文件IO总结

标准IO

文件IO

概念

C库中定义的一组用于输入输出的函数

posix中定义的一组输入输出的函数

特点

  1. 有缓冲机制
  2. 围绕流进行操作,FILE*
  3. 默认打开三个流:stdin/stdout/stderr
  4. 只能操作普通文件
  5. 程序可移植性相对更强
  1. 没有缓冲机制
  2. 围绕文件描述符进行操作,非负整数
  3. 默认打开三个文件描述符:0/1/2
  4. 可以操作除了目录以外任意类型文件
  5. 可移植性相对较弱

函数

打开文件:fopen

关闭文件:fclose

读文件: fgets/fread

写文件: fputs/fwrite

定位操作:rewind/fseek/ftell

打开文件:open

关闭文件: close

读文件:read

写文件:write

定位操作: lseek

头文件: include <stdio.h>

<>代表去系统路径下查找头文件/usr/include

#include"head.h"

""代表下去当前路径下查找,找不到再去系统路径下查找

头文件也就是以.h结尾的文件,其中包含:结构体、共用体、枚举的定义,宏定义,函数声明,外部引用,typedef重定义,其他头文件

源文件:

包含main函数的.c文件

包含其他子函数的.c文件,封装的函数需要在头文件中声明

库文件(不能包含main函数)

  1. 库的定义

当使用别人的函数的时候除了用头文件以外还需要有库

头文件:宏定义、结构体、共用体、枚举的定义、函数声明、外部引用、重定义、其他头文件等。

库:把一些常用的函数的目标文件打包在一起,提供相应的函数接口,便于程序员使用。本质上来说库是一种可执行代码的二进制形式文件

  1. 库的分类

静态库和动态库,本质的区别代码载入时刻不同。

2.1 静态库

静态库在程序编译时会被复制到目标代码中,以.a结尾。

优点: 程序运行时将不再需要静态库,运行时无需加载库,运行速度快; 可移植性好。

缺点: 静态库的代码复制到了程序中,程序的体积因此较大; 静态库升级后,程序需要重新编译链接。

2.2 动态库

动态库是在程序运行时才被载入代码中,也叫共享库,以.so结尾。

优点:程序在执行时加载动态库,程序代码体积小; 程序升级更简单。

不同的应用程序如果调用相同的库,那么在内存中只需要有一份共享库就可以了。

缺点:运行时还需要加载库,运行速度慢; 可移植性相对较差。

  1. 静态库的制作
  1. 将源文件编译生成目标文件

gcc -c fun.c -o fun.o

  1. 创建静态库用ar命令,将很多个.o转换成.a

ar crs libfun.a fun.o

静态库文件名的命名规范是以lib为前缀,紧接着跟静态库名,扩展名为.a

  1. 测试使用静态库

gcc main.c -L. -lfun //-L指定库的路径 -l指定库名字

执行 ./a.out

  1. 动态库的制作
  1. 用gcc来创建共享库

gcc -fPIC -c fun.c -o fun.o //-fPIC创建与地址无关的编译程序

gcc -shared fun.o -o libmyfun.so //生成动态库

  1. 测试使用动态库

gcc main.c -L. -lmyfun

./a.out: 可以正常编译通过,但是运行时报错 ./a.out: error while loading shared libraries: libmyfun.so: cannot open shared object file: No such file or directory

原因:当加载动态库,系统会默认从/lib或/usr/lib路径下查找库文件,所以不用-L指定路径,直接gcc main.c -lmyfun就可以了

解决方法(有三种):

  1. 把库拷贝到/usr/lib和/lib目录下。(此方法编译时不需要指定库的路径)
  2. 在LD_LIBRARY_PATH环境变量中加上库所在路径。

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.

(终端关闭,环境变量就没在了)

  1. 添加/etc/ld.so.conf.d/*.conf文件。把库所在的路径加到文件末尾,并执行ldconfig刷新

sudo vi xx.conf

添加动态库存在的路径,如:

/home/hq/work/lib

选项:

-L路径:指定库的路径

-l(小写L)库名: 指定库的名字

-I(大写i) 路径:指定头文件的路径,默认查找的路径/usr/include

<>代表从系统路径下/usr/include查找头文件

""代表从当前目录下查找头文件,如果没有再去系统下查找

ldd 可执行文件名:查看链接的动态库

补充:

同名的静态库和动态库:默认优先使用动态库,如果想使用静态库 需要在后面加 -static,这是内核规定的。

如果链接没有lib前缀的库文件,可以直接用-指定库的全名无需加l选项。

  1. 总结静态库和动态库

静态库:编译阶段,以.a结尾,速度快,体积大,移植性好,升级麻烦。

动态库:执行阶段,以.so结尾,速度慢,体积小,移植性较弱,升级简单。

可以看出用静态库编译出来的程序体积大:

升级演示:改变库的源文件,重新制作库。

静态库升级需要重新编译链接

动态库的升级不需要重新编译链接了

进程 Process

1.什么是进程

进程和程序的区别

1.1概念

程序:编译好的可执行文件

存放在磁盘上的指令和数据的有序集合(文件)

程序时静态的,没有任何执行的概念

进程:一个独立的可调度的任务

执行一个程序所分配的资源的总称

进程是程序执行的一次过程

进程是动态的,包括创建、调度、执行、消亡

1.2 特点

  1. 系统会为每一个进程分配0-4g的虚拟空间,0-3g(用户空间)是每个进程所独有的,

3-4g(内核空间)是所有进程所共有的。

进程间通信:

==》

(2)CPU调度进程时会给进程分配时间片(几毫秒~十几毫秒),当时间片用完后,CPU再去进行其他进程调度,实现进程的轮转,从而实现多任务的操作。(没有外界干预的情况下怎么调度进程是CPU随机分配的)

进程控制块task_struct(了解)

  • 进程控制块pcb: 包含描述进程的相关信息
  • 进程标识PID:唯一的标识一个进程

主要进程标识:

进程号(PID: Process Identity Number)

父进程号:(Parent Process ID: PPID)

  • 进程用户
  • 进程状态、优先级
  • 文件描述符(记录当前进程打开的文件)

1.3 进程段

Linux中的进程大致包含三个段:

数据段存放的是全局变量、常数以及动态数据分配的数据空间(如malloc函数取得的空间)等。

正文段:存放的是程序中的代码

堆栈段存放的是函数的返回地址、函数的参数以及程序中的局部变量 (类比内存的栈区)

1.4 进程分类

交互进程:该类进程是由shell控制和运行的。交互进程既可以在前台运行,也可以在后台运行。该类进程经常与用户进行交互,需要等待用户的输入,当接收到用户的输入后,该类进程会立刻响应,典型的交互式进程有:shell命令进程、文本编辑器等

批处理进程:该类进程不属于某个终端,它被提交到一个队列中以便顺序执行。(目前接触不到)

守护进程:该类进程在后台运行。它一般在Linux启动时开始执行,系统关闭时才结束。

【腾讯文档】守护进程的概念和创建

守护进程的概念和创建

1.5 进程状态

D uninterruptible sleep (usually IO) 不可中断的睡眠态

R running or runnable (on run queue) 运行态

S interruptible sleep (waiting for an event to complete) 可中断的睡眠态

T stopped by job control signal 暂停态

t stopped by debugger during the tracing 因为调试而暂停

X dead (should never be seen) 死亡态

Z defunct ("zombie") process, terminated but not reaped by its parent 僵尸态

< high-priority (not nice to other users) 高优先级

N low-priority (nice to other users) 低优先级

L has pages locked into memory (for real-time and custom IO) 锁在内存中

s is a session leader 会话组组长

l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)多线程

+ is in the foreground process group 前台进程

没有+时,默认是后台进程

I (大写i)空闲状态进程

1.6 进程状态切换图

进程创建后,进程进入就绪态,当CPU调度到此进程时进入运行态,当时间片用完时,此进程会进入就绪态,如果此进程正在执行一些IO操作(阻塞操作)会进入阻塞态,完成IO操作(阻塞结束)后又可进入就绪态,等待CPU的调度,当进程运行结束即进入结束态。

什么是阻塞和非阻塞?

阻塞(blocking)、非阻塞(non-blocking):可以简单理解为需要做一件事能不能立即得到返回应答,如果不能立即获得返回,需要等待,那就阻塞了,在等待的过程中可以做其它事情。否则就可以理解为非阻塞。

1.7 进程相关命令

ps 查看系统中的进程 -aux -ef

top 动态显示系统中的进程

nice 按用户指定的优先级运行进程

renice 改变正在运行进程的优先级

kill 给进程发送信号

fg 将进程切换到前台执行

bg 将进程切换到后台执行

jobs 查看当前终端后台进程

作业:

  1. 吸收今天所学内容,该记住的记住,代码敲至少2遍。
  2. 录制视频:同位互相提问,由组长汇总到群里,反馈情况。
  3. 用标准IO实现cp功能

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

int main(int argc, char const *argv[])
{
    FILE *fp_src, *fp_dest;
    char buf[32] = "";
    int n = 0;
    if (argc != 3)
    {
        printf("pls input:%s <srcname> <destname>\n", argv[0]);
        return -1;
    }
    fp_src = fopen(argv[1], "r");
    if (fp_src == NULL)
    {
        perror("fopen srcfile err");
        return -1;
    }

    fp_dest = fopen(argv[2], "w");
    if (fp_dest == NULL)
    {
        perror("fopen destfile err");
        return -1;
    }
    
    while (fgets(buf, 32, fp_src) != NULL)
        fputs(buf, fp_dest);

    fclose(fp_src);
    fclose(fp_dest);
    return 0;
}

2.进程函数接口

2.1 创建进程 fork()

pid_t fork(void);
功能:创建子进程
返回值:
    成功:在父进程中:返回子进程的进程号 >0
         在子进程中:返回值为0
    失败:-1并设置errno

解释:./a.out会启动一个进程,执行到fork()函数时会在当前进程中创造了一个子进程并把代码以及数据信息拷贝到子进程,这两个进程只有个别数据例如进程号不一样,此时这两个进程由CPU随机调度。注意!!子进程会得到fork函数返回值然后执行fork之后的代码,fork函数之前的代码不会执行。

特点:

1)子进程几乎拷贝了父进程的全部内容。包括代码、数据、系统数据段中的pc值、栈中的数据、父进程中打开的文件等;但它们的PID、PPID是不同的。

2)父子进程有独立的地址空间,互不影响;当在相应的进程中改变全局变量、静态变量,都互不影响。

3)若父进程先结束,子进程成为孤儿进程,被init进程收养,子进程变成后台进程。

4)若子进程先结束,父进程如果没有及时回收资源,子进程变成僵尸进程(要避免僵尸进程产生)

2.2 回收资源

pid_t wait(int *status);
功能:回收子进程资源(阻塞)
参数:status:子进程退出状态,不接受子进程状态设为NULL
返回值:成功:回收的子进程的进程号
              失败:-1
              
pid_t waitpid(pid_t pid, int *status, int options);
功能:回收子进程资源
参数:
    pid:>0     指定子进程进程号
         =-1   任意子进程
         =0    等待其组ID等于调用进程的组ID的任一子进程
         <-1   等待其组ID等于pid的绝对值的任一子进程
    status:子进程退出状态
    options:0:阻塞 WNOHANG:非阻塞
返回值:正常:结束的子进程的进程号
      当使用选项WNOHANG且没有子进程结束时:0
      出错:-1

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

int main(int argc, char const *argv[])
{

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

    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0)
    {
        printf("子进程\n");
        sleep(2);
    }
    else
    {
        // wait(NULL); //阻塞等待回收子进程资源
        // waitpid(-1,NULL,0); //0:阻塞等待回收子进程资源
        // waitpid(-1,NULL,WNOHANG); //WNOHANG:非阻塞
        // 设置为非阻塞有可能回收不到子进程资源,所以可以用轮询
        while (1)
        {
            //回收到资源就结束循环
            if (waitpid(-1, NULL, WNOHANG) > 0)
                break;
        }

        printf("父进程\n");
        while (1) // 让进程不要结束
            ;
    }

    return 0;
}

2.3 结束进程

void exit(int status);
功能:结束进程,刷新缓存
void _exit(int status);
功能:结束进程,不刷新缓存
参数:status是一个整型的参数,可以利用这个参数传递进程结束时的状态。
    通常0表示正常结束;
其他的数值表示出现了错误,进程非正常结束
    printf("hello");
    // exit(0); //结束进程并且刷新缓存
    _exit(0);  //结束进程并且不刷新缓存
    while(1);

exit是结束当前进程,return结束当前函数

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

int fun()
{
    printf("in fun\n");
    exit(0);   //结束整个进程
    //return 0;  //结束当前函数
}

int main(int argc, char const *argv[])
{
    printf("hello\n");
    fun();
    printf("666\n");

    return 0;
}

2.4 获取进程号

pid_t getpid(void);
功能:获取当前进程的进程号
pid_t getppid(void);
功能:获取当前进程的父进程号

例如:父子进程中分别打印这两个进程的进程号

进程间通信 IPC

InterProcess Communication

1.进程间通信方式IPC

  1. 早期的进程间通信:

无名管道(pipe)、有名管道(fifo)、信号(signal)

  1. systerm V IPC:

共享内存(share memory)、信号灯集(semaphore)、消息队列(message queue)

  1. BSD:

套接字(socket)

进程间通信

2.无名管道

2.1 特点

  1. 只能用于具有亲缘关系的进程之间的通信
  2. 半双工的通信模式,具有固定的读端fd[0]和写端fd[1]。
  3. 管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。
  4. 管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符 fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。

2.2 函数接口

int pipe(int fd[2])
功能:创建无名管道
参数:文件描述符 fd[0]:读端  fd[1]:写端
返回值:成功 0
       失败 -1

3.有名管道

3.1 特点

  1. 有名管道可以使互不相关的两个进程互相通信。
  2. 有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。但是读写数据不会存在文件中,而是在管道中。
  3. 进程通过文件IO来操作有名管道
  4. 有名管道遵循先进先出规则
  5. 不支持如lseek() 操作

3.2 函数接口

man 3 mkfifo

int mkfifo(const char *filename,mode_t mode);
功能:创健有名管道
参数:filename:有名管道文件名
       mode:权限
返回值:成功:0
       失败:-1,并设置errno号
注意对错误的处理方式:
如果错误是file exist时,注意加判断,如:if(errno == EEXIST)

注意:函数只是在指定的路径下创建管道文件,但是读写的数据在内核空间中。

步骤: 先mkfifo创建管道文件、然后通过open打开文件获得文件描述符、通过read/write进行读写

4.信号

kill -l: 显示系统中的信号

kill -num PID: 给某个进程发送信号

4.1 概念

1)信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式

2)信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。

3)如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

4.2 信号的响应方式

1)忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。

2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。

3)执行缺省操作:Linux对每种信号都规定了默认操作

4.3信号种类

SIGINT(2):中断信号,Ctrl-C 产生,用于中断进程

SIGQUIT(3):退出信号, Ctrl-\ 产生,用于退出进程并生成核心转储文件

SIGKILL(9):终止信号,用于强制终止进程。此信号不能被捕获或忽略。

SIGALRM(14):闹钟信号,当由 alarm() 函数设置的定时器超时时产生。

SIGTERM(15):终止信号,用于请求终止进程。此信号可以被捕获或忽略。termination

SIGCHLD(17):子进程状态改变信号,当子进程停止或终止时产生。

SIGCONT(18):继续执行信号,用于恢复先前停止的进程。

SIGSTOP(19):停止执行信号,用于强制停止进程。此信号不能被捕获或忽略。

SIGTSTP(20):键盘停止信号,通常由用户按下 Ctrl-Z 产生,用于请求停止进程。

4.4 函数接口

4.4.1 信号发送和挂起

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
功能:信号发送
参数:pid:指定进程
   sig:要发送的信号
返回值:成功 0     
       失败 -1
       
int raise(int sig);
功能:进程向自己发送信号
参数:sig:信号
返回值:成功 0   
       失败 -1
相当于:kill(getpid(), sig);

int pause(void);
功能:用于将调用进程挂起,直到收到被捕获处理的信号为止。
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    // kill(getpid(), SIGKILL); //给指定进程发送信号
    // raise(SIGKILL); //给当前进程发送信号

    // while (1)
    //     ;
    pause(); //将进程挂起,作用和死循环类似,但是不占用CPU

    return 0;
}

4.4.2 定时器

unsigned int alarm(unsigned int seconds)
功能:在进程中设置一个定时器。当定时器指定的时间到了时,它就向进程发送SIGALRM信号。
参数:seconds:定时时间,单位为秒
返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
注意:一个进程只能有一个闹钟时间。如果在调用alarm时已设置过闹钟时间,则之前的闹钟时间被新值所代替。
常用操作:取消定时器alarm(0),返回旧闹钟余下秒数。

系统默认对SIGALRM(闹钟到点后内核发送的信号)信号的响应: 如果不对SIGALRM信号进行捕捉或采取措施,默认情况下,闹钟响铃时刻会退出进程。

例子:

    printf("%d\n", alarm(10));  //0
    sleep(2); //睡眠2秒闹钟还剩余8秒
    printf("%d\n", alarm(3)); //8
    //新闹钟会代替旧闹钟,以最后一次闹钟为准
    pause(); //3秒后收到SIGALRM信号进程结束

4.4.3 信号处理函数signal()

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数:signum:要处理的信号
      handler:信号处理方式
          SIG_IGN:忽略信号  (忽略 ignore)
          SIG_DFL:执行默认操作 (默认 default)
          handler:捕捉信号 (handler为函数名,可以自定义)
     void handler(int sig){} //函数名可以自定义, 参数为要处理的信号
返回值:成功:设置之前的信号处理方式
      失败:-1

补充: typedef给数据类型重定义

#include <stdio.h>

//给普通数据类型int重命名
typedef int size4;        //int a;

//给指针类型int* 重命名
typedef int *int_p;      //int *p; 

//给数组类型int [10]重命名
typedef int intArr10[10];  //int a[10];

//给函数指针void (*)()重命名
typedef void (*fun_p)(); //void (*p)();

void fun()
{
    printf("fun\n");
}

int main(int argc, char const *argv[])
{
    size4 a = 10;             //相当于int a=10;
    int_p p = &a;             //相当于int* p=&a;
    intArr10 arr = {1, 2, 3}; //相当于int arr[10]={1,2,3};
    fun_p fp = fun;           //相当于 void (*fp)()=fun;
    printf("%d\n", *p);
    printf("%d\n", arr[0]);
    fp();

    return 0;
}

总而言之,定义变量的变量名写在哪里,用typedef给数据类型重命名的新名字就写在哪里。然后使用新名字定义变量的格式直接就可以为:新名字 变量名;

例子:

#include <signal.h>
#include <stdio.h>

#include <unistd.h>

void handler(int sig) // sig代表的就是处理的信号
{
    if (sig == SIGINT)
        printf("ctrl c:%d\n", sig);
    if (sig == SIGTSTP)
        printf("ctrl z:%d\n", sig);
}

int main(int argc, char const *argv[])
{
    // signal(SIGINT, SIG_IGN); // 忽略信号
    // signal(SIGINT, SIG_DFL); // 缺省方式
    signal(SIGINT, handler); // 捕捉信号,比较常用的方式
    signal(SIGTSTP, handler);
    while (1) //循环挂起,不占用CPU
        pause(); // 收到了被捕获方式处理的信号后就结束挂起
    printf("hello\n");

    return 0;
}

例子:

用信号的知识实现司机和售票员问题。

1)售票员捕捉SIGINT(代表开车)信号,向司机发送SIGUSR1信号,司机打印(let's gogogo)

2)售票员捕捉SIGQUIT(代表停车)信号,向司机发送SIGUSR2信号,司机打印(stop the bus)

3)司机捕捉SIGTSTP(代表到达终点站)信号,向售票员发送SIGUSR1信号,售票员打印(please get off the bus)

4)司机等待售票员下车,之后司机再下车。

分析:司机(父进程)、售票员(子进程)

售票员:捕捉:SIGINT、SIGQUIT、SIGUSR1

忽略: SIGTSTP

司机:捕捉:SIGUSR1、SIGUSR2、SIGTSTP

忽略:SIGINT、SIGQUIT

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
pid_t pid; // 全局变量作用域是整个程序

void saler(int sig)
{
    if (sig == SIGINT)
        kill(getppid(), SIGUSR1);
    if (sig == SIGQUIT)
        kill(getppid(), SIGUSR2);
    if(sig==SIGUSR1)
    {
        printf("please get off the bus!\n");
        exit(0); //让售票员下车
    }
}

void driver(int sig)
{
    if (sig == SIGUSR1)
        printf("lets gogogo!\n");
    if (sig == SIGUSR2)
        printf("stop the bus!\n");
    if (sig == SIGTSTP)
    {
        kill(pid, SIGUSR1);
        wait(NULL);  //等待子进程结束以后

        exit(0);  //让司机也下车
    }
}

int main(int argc, char const *argv[])
{
    pid = fork();
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0)
    {
        printf("hi~ i am saler:%d\n", getpid());
        signal(SIGINT, saler);
        signal(SIGQUIT, saler);
        signal(SIGUSR1, saler);
        signal(SIGTSTP, SIG_IGN);
    }
    else
    {
        printf("hi~ i am driver:%d\n", getpid());
        signal(SIGINT, SIG_IGN);
        signal(SIGQUIT, SIG_IGN);
        signal(SIGUSR1, driver);
        signal(SIGUSR2, driver);
        signal(SIGTSTP, driver);
    }

    while (1) // 不能收到一个信号就结束挂起,所以可以循环挂起
        pause();

    return 0;
}

5.共享内存

5.1 特点

1)共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。

2)为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程

将其映射到自己的私有地址空间。进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。

  1. 由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等

5.2 步骤

  1. 创建key值
  2. 创建或打开共享内存
  3. 映射共享内存到用户空间
  4. 撤销映射
  5. 删除共享内存

5.3 函数接口

key_t ftok(const char *pathname, int proj_id);
功能:创建出来的具有唯一映射关系的一个key值,帮助操作系统用来标识一块共享内存
参数:
    Pathname:已经存在的可访问文件的名字
    Proj_id:一个字符(因为只用低8位)
返回值:成功:key值
      失败:-1

int shmget(key_t key, size_t size, int shmflg);
功能:创建或打开共享内存
参数:
    key  键值
    size   共享内存的大小
    shmflg   IPC_CREAT|IPC_EXCL|0777
返回值:成功  shmid
      出错    -1
注意对错误的处理方式:
如果错误是file exist光打开共享内存不用设IPC_CREAT|IPC_EXCL了,加判断,如:if(errno == EEXIST)

void  *shmat(int  shmid,const  void  *shmaddr,int  shmflg); //attaches
功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
参数:
    shmid   共享内存的id号
    shmaddr   一般为NULL,表示由系统自动完成映射
              如果不为NULL,那么有用户指定
    shmflg:SHM_RDONLY就是对该共享内存只进行读操作
                0     可读可写
返回值:成功:完成映射后的地址,
       出错:-1(地址)
用法:if((p = (char *)shmat(shmid,NULL,0)) == (char *)-1)

int shmdt(const void *shmaddr); //detaches
功能:取消映射
参数:要取消的地址
返回值:成功0  
      失败的-1

int  shmctl(int  shmid,int  cmd,struct shmid_ds *buf); //control
功能:(删除共享内存),对共享内存进行各种操作
参数:
    shmid   共享内存的id号
    cmd     IPC_STAT 获得shmid属性信息,存放在第三参数
            IPC_SET 设置shmid属性信息,要设置的属性放在第三参数
            IPC_RMID:删除共享内存,此时第三个参数为NULL即可
    buf    shmid所指向的共享内存的地址,空间被释放以后地址就赋值为null
返回:成功0 
     失败-1
用法:shmctl(shmid,IPC_RMID,NULL);

5.4. 命令:

ipcs -m: 查看系统中的共享内存

ipcrm -m shmid:删除共享内存

ps: 可能不能直接删除掉还存在进程使用的共享内存。

这时候可以用ps -ef对进程进行查看,kill掉多余的进程后,再使用ipcs查看。

#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <errno.h>

int main(int argc, char const *argv[])
{
    // 1.创建key值
    key_t key;
    int shmid;
    key = ftok("./shm.c", 'd');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key=%#x\n", key);

    // 2.打开或创建共享内存
    shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0777);
    if (shmid < 0)
    {
        // 不能把已存在当成错误,所以要对容错判断做处理。如果已存在就直接打开共享内存。
        if (errno == EEXIST) // 别忘了errno.h头文件
            shmid = shmget(key, 128, 0777);
        else // 其他的错误是真错误
        {
            perror("shmget err");
            return -1;
        }
    }
    printf("shmid:%d\n", shmid);

    // 3.映射共享内存
    char *p = (char *)shmat(shmid, NULL, 0);
    if (p == (char *)-1)
    {
        perror("shmat err");
        return -1;
    }

    // 4.操作共享内存
    scanf("%s", p);
    printf("%s\n", p);

    // 5.撤销映射
    shmdt(p);

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

    return 0;
}

利用共享内存进行进程间通信:

利用相同的key使用相同的共享内存

作业:

练习:两个进程实现通信,一个进程循环从终端输入,另一个进程循环打印,当输入quit时结束

这两个标志在两个进程里,是不共享的,所以为了共享标志位可以和buf封装到一个结构体里作为共享内存。

struct msg

{

int flag;

char buf[32];

};

scanf端:

#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
struct msg
{
    int flag;
    char buf[32];
};

int main(int argc, char const *argv[])
{
    // 1.创建key值
    key_t key;
    int shmid;
    key = ftok("./shm.c", 'd');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key=%#x\n", key);

    // 2.打开或创建共享内存
    shmid = shmget(key, sizeof(struct msg), IPC_CREAT | IPC_EXCL | 0777);
    if (shmid < 0)
    {
        // 不能把已存在当成错误,所以要对容错判断做处理。如果已存在就直接打开共享内存。
        if (errno == EEXIST) // 别忘了errno.h头文件
            shmid = shmget(key, sizeof(struct msg), 0777);
        else // 其他的错误是真错误
        {
            perror("shmget err");
            return -1;
        }
    }
    printf("shmid:%d\n", shmid);

    // 3.映射共享内存
    struct msg *p = (struct msg *)shmat(shmid, NULL, 0);
    if (p == (struct msg *)-1)
    {
        perror("shmat err");
        return -1;
    }

    // 4.操作共享内存
    p->flag = 0;
    while (1)
    {
        if (p->flag == 0)
        {
            scanf("%s", p->buf);
            p->flag = 1;
            if (strcmp(p->buf, "quit") == 0)
                break;
        }
    }
    // 5.撤销映射
    shmdt(p);

    return 0;
}

printf端:

#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
struct msg
{
    int flag;
    char buf[32];
};

int main(int argc, char const *argv[])
{
    // 1.创建key值
    key_t key;
    int shmid;
    key = ftok("./shm.c", 'd');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key=%#x\n", key);

    // 2.打开或创建共享内存
    shmid = shmget(key, sizeof(struct msg), IPC_CREAT | IPC_EXCL | 0777);
    if (shmid < 0)
    {
        // 不能把已存在当成错误,所以要对容错判断做处理。如果已存在就直接打开共享内存。
        if (errno == EEXIST) // 别忘了errno.h头文件
            shmid = shmget(key, sizeof(struct msg), 0777);
        else // 其他的错误是真错误
        {
            perror("shmget err");
            return -1;
        }
    }
    printf("shmid:%d\n", shmid);

    // 3.映射共享内存
    struct msg *p = (struct msg *)shmat(shmid, NULL, 0);
    if (p == (struct msg *)-1)
    {
        perror("shmat err");
        return -1;
    }

    // 4.操作共享内存
    p->flag = 0;
    while (1)
    {

        if (p->flag == 1)
        {
            printf("%s\n", p->buf);
            p->flag = 0;
            if (strcmp(p->buf, "quit") == 0)
                break;
        }
    }
    //撤销映射
    shmdt(p);

    //删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

6.信号灯集

6.1 特点

信号灯(semaphore),也叫信号量,信号灯集是一个信号灯的集合。它是不同进程间或一个给定进程内部不同线程间同步的机制;

而Posix信号灯指的是单个计数信号灯:无名信号灯、有名信号灯。(咱们学的是无名信号灯)

System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。

通过信号灯集实现共享内存的同步操作

6.2 步骤

  1. 创建key值:ftok
  2. 创建或打开信号灯集: semget
  3. 初始化信号灯: semctl
  4. PV操作:semop
  5. 删除信号灯集: semctl

6.3. 命令

ipcs -s:查看信号灯集

ipcrm -s semid:删除信号灯集

注意:有时候可能会创建失败,或者semid为0,所以用命令看看,删了重新创建就可以了。

6.4 函数接口

int semget(key_t key, int nsems, int semflg);
功能:创建/打开信号灯
参数:key:ftok产生的key值
    nsems:信号灯集中包含的信号灯数目
    semflg:信号灯集的访问权限,通常为IPC_CREAT|IPC_EXCL|0666
返回值:成功:信号灯集ID
       失败:-1
       
int semctl ( int semid, int semnum,  int cmd…/*union semun arg*/);
功能:信号灯集合的控制(初始化/删除)
参数:semid:信号灯集ID
    semnum: 要操作的集合中的信号灯编号,信号灯编号从0开始
     cmd: 
        GETVAL:获取信号灯的值,返回值是获得值
        SETVAL:设置信号灯的值,需要用到第四个参数:共用体
        IPC_RMID:从系统中删除信号灯集合
返回值:成功 0
      失败 -1
用法:
1. 初始化信号灯集:
需要自定义共用体
union semun{
    int val;
} mysemun;
mysemun.val = 10;
semctl(semid, 0, SETVAL, mysemun);

2. 获取信号灯值:函数semctl(semid, 0, GETVAL)的返回值
3. 删除信号灯集:semctl(semid, 0, IPC_RMID);


int semop ( int semid, struct sembuf  *opsptr,  size_t  nops);
功能:对信号灯集合中的信号量进行PV操作
参数:semid:信号灯集ID
     opsptr:操作方式
     nops:  要操作的信号灯的个数 1个
返回值:成功 :0
      失败:-1
struct sembuf {
   short  sem_num; // 要操作的信号灯的编号
   short  sem_op;  //    0 :  等待,直到信号灯的值变成0
                   //   1  :  释放资源,V操作
                   //   -1 :  申请资源,P操作                    
    short  sem_flg; // 0(阻塞),IPC_NOWAIT, SEM_UNDO
};

用法:
申请资源 P操作:
    mysembuf.sem_num = 0;
    mysembuf.sem_op = -1;
    mysembuf.sem_flg = 0;
    semop(semid, &mysembuf, 1);
释放资源 V操作:
    mysembuf.sem_num = 0;
    mysembuf.sem_op = 1;
    mysembuf.sem_flg = 0;
    semop(semid, &mysembuf, 1);


7.消息队列

传统:无名管道、有名管道、信号

system V: 共享内存、信号灯集、消息队列

7.1 特点

消息队列是IPC对象(活动在内核级别的一种进程间通信的工具)的一种

一个消息队列由一个标识符 (即队列ID)来标识

消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等

消息队列可以按照类型(自己设一个值作为类型)来发送/接收消息

7.2 步骤

  1. 创建key值 ftok
  2. 创建或打开消息队列 msgget
  3. 添加消息:按照消息类型把消息添加到打开的消息队列末尾 msgsnd
  4. 读取消息:可以按照消息类型把消息从队列中取走 msgrcv
  5. 删除消息队列 msgctl

7.3 操作命令

ipcs -q:查看消息队列

ipcrm -q msgid: 删除消息队列

注意:有时候可能会创建失败,或者msgid为0,所以用命令看看,删了重新创建就可以了。

7.4 函数接口

int msgget(key_t key, int flag);
功能:创建或打开一个消息队列
参数:  key值
       flag:创建消息队列的权限IPC_CREAT|IPC_EXCL|0666
返回值:成功:msgid
       失败:-1
int msgsnd(int msgid, const void *msgp, size_t size, int flag); 
功能:添加消息
参数:msgid:消息队列的ID
      msgp:指向消息的指针。常用消息结构msgbuf如下:
          struct msgbuf{
            long mtype;          //消息类型
            char mtext[N]};     //消息正文
   size:发送的消息正文的字节数
   flag:IPC_NOWAIT消息没有发送完成函数也会立即返回    
         0:直到发送完成函数才返回
返回值:成功:0
      失败:-1
使用:msgsnd(msgid, &msg,sizeof(msg)-sizeof(long), 0)
注意:消息结构除了第一个成员必须为long类型外,其他成员可以根据应用的需求自行定义。

int msgrcv(int msgid,  void* msgp,  size_t  size,  long msgtype,  int  flag);
功能:读取消息
参数:msgid:消息队列的ID
     msgp:存放读取消息的空间
     size:接受的消息正文的字节数(sizeof(msgp)-sizeof(long))
    msgtype:
            0:接收消息队列中第一个消息。
            大于0:接收消息队列中第一个类型为msgtyp的消息.
            小于0:接收消息队列中类型值不小于msgtyp的绝对值且类型值又最小的消息。
     flag:
           0:若无消息函数会一直阻塞
           IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG
返回值:成功:接收到的消息的长度
      失败:-1
      
int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );
功能:对消息队列的操作,删除消息队列
参数:msqid:消息队列的队列ID
     cmd:
        IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。
        IPC_SET:设置消息队列的属性。这个值取自buf参数。
        IPC_RMID:从系统中删除消息队列。
     buf:消息队列缓冲区
返回值:成功:0
      失败:-1
用法:msgctl(msgid, IPC_RMID, NULL);

#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <errno.h>
#include <sys/msg.h>

// 消息结构体类型
struct msgbuf
{
    long type; // 必须有!表示消息类型,值>0
    int num;   // 消息正文,根据自己需求定义
    char ch;
};

int main(int argc, char const *argv[])
{
    int msgid;
    // 1.创建key值
    key_t key = ftok("msg.c", 'a');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key=%#x\n", key);
    // 2.创建或打开消息队列
    msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
    if (msgid < 0)
    {
        if (errno == EEXIST)
            msgid = msgget(key, 0666);
        else
        {
            perror("msgget err");
            return -1;
        }
    }
    printf("msgid=%d\n", msgid);

    // 3.添加读取消息
    // 添加消息
    struct msgbuf msg;
    msg.type = 1;
    msg.num = 10;
    msg.ch = 'a';
    msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0);

    msg.type = 2;
    msg.num = 20;
    msg.ch = 'b';
    msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0);

    // 读取消息
    struct msgbuf m;
    msgrcv(msgid, &m, sizeof(m) - sizeof(long), 2, 0); // 0:阻塞,读完再返回
    printf("%d %c\n", m.num, m.ch);

    //删除消息队列
    msgctl(msgid,IPC_RMID,NULL);

    return 0;
}

利用消息队列进程间通信:

线程 Thread

1.什么是线程

1.1 概念

线程是一个轻量级的进程,为了提高系统的性能引入线程。

线程和进程都参与统一调度。

在同一个进程中可以创建多个线程,共享进程资源。

1.2 进程和线程的区别(面试题)

相同点:都为操作系统提供并发执行的能力

不同点:

调度和资源:线程是系统调度的最小单位; 进程是资源分配的最小单位。

地址空间:同一进程的多个线程共享该进程的资源; 进程的地址空间相对独立。

通信方面:线程通信相对简单,只需要通过全局变量就可以实现,但是需要考虑临界资源访问问题;进程通信比较复杂,需要借助进程的通信机制(3~4g的内核空间)。

安全性: 线程安全性差一些,当进程结束时会导致所有的线程退出; 进程相对安全。

1.3 线程资源

共享的资源:可执行的指令、静态数据、进程中打开的文件描述符、信号处理函数、当前工作目录、用户ID、用户组ID

私有的资源:线程ID (TID)、PC(程序计数器)和相关寄存器、堆栈(局部变量, 返回地址)、错误号 (errno)、信号掩码和优先级、执行状态和属性

2.函数接口

2.1 创建线程: pthread_create

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
                                void *(*start_routine) (void *), void *arg);
功能:创建线程
参数:       thread:线程标识
            attr:线程属性, NULL:代表设置默认属性
            start_routine:函数名:代表线程函数(自己写的)
            arg:用来给前面函数传参
返回值:成功:0
      失败:错误码
      
编译的时候需要加 -pthread 链接动态库

函数指针例子:

#include <stdio.h>
#include <stdlib.h>
int test(int (*p)(int, int), int a, int b)//p=fun, a=3, b=4
{
    return p(a,b); //通过函数指针调用所指函数
    //fun(3,4);
}
int fun(int n, int m) //n=3,m=4
{
    return n * m; //3*4
}

int main(int argc, char const *argv[])
{
      printf("%d\n", test(fun, 3, 4)); //12
      return 0;
}

例子:

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

void *handler(void *arg)
{
    printf("in handler\n");
    while(1); //让从线程不要退出
    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t tid;
    if( pthread_create(&tid,NULL,handler,NULL)!=0)
    {
        perror("create err");
        return -1;
    }
    printf("in main\n");
    while(1);  //让主线程也就是进程不要结束
    return 0;
}

2.2 退出线程: pthread_exit

void  pthread_exit(void *value_ptr) 
功能:用于退出线程的执行
参数:value_ptr:线程退出时返回的值

例子:

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

void *handler(void *arg)
{
    printf("in handler\n");
    pthread_exit(NULL);  //退出从线程
    while(1); //让从线程不要退出
    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t tid;
    if( pthread_create(&tid,NULL,handler,NULL)!=0)
    {
        perror("create err");
        return -1;
    }
    printf("in main\n");
    while(1);  //让主线程也就是进程不要结束
    return 0;
}

2.3 回收线程资源

int  pthread_join(pthread_t thread,  void **value_ptr) 
功能:用于等待一个指定的线程结束,阻塞函数
参数:thread:创建的线程对象,线程ID
     value_ptr:指针*value_ptr  一般为NULL
返回值:成功 : 0
       失败:errno
       
int pthread_detach(pthread_t thread);
功能:让线程结束时自动回收线程资源,让线程和主线程分离,非阻塞函数
参数:thread:线程ID
非阻塞式的,例如主线程分离(detach)了线程T2,那么主线程不会阻塞在pthread_detach(),pthread_detach()会直接返回,线程T2终止后会被操作系统自动回收资源

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

void *handler(void *arg)
{
    printf("in handler\n");
    sleep(2);
    pthread_exit(NULL); // 退出从线程
    while (1)
        ; // 让从线程不要退出
    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, handler, NULL) != 0)
    {
        perror("create err");
        return -1;
    }

    // pthread_join(tid, NULL); //阻塞等待指定线程退出回收其资源
    pthread_detach(tid); //让指定线程为分离态,结束时自动回收资源给系统,不阻塞。
    printf("in main\n");
    while (1)
        ; // 让主线程也就是进程不要结束
    return 0;
}

2.4 获取线程号: pthread_self

pthread_t pthread_self(void);
功能:
    获取线程号
返回值:
    成功:调用此函数线程的ID
#include <pthread.h>
#include <stdio.h>

void *handler(void *arg)
{
    printf("in handler:%lu\n", pthread_self());
    pthread_exit(NULL); // 退出从线程
    while (1)
        ; // 让从线程不要退出
    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, handler, NULL) != 0)
    {
        perror("create err");
        return -1;
    }
    printf("in main: %lu\n", tid);
    while (1)
        ; // 让主线程也就是进程不要结束
    return 0;
}

3.同步

3.1 概念

同步(synchronization)指的是多个任务(线程)按照约定的顺序相互配合完成一件事情

(异步:异步则反之,并非一定要按照顺序去完成)

3.2 同步机制

通过信号量实现线程同步

信号量:通过信号量实现同步操作; 由信号量来决定线程继续运行还是阻塞等待。

信号量: 代表一类资源,其值可以表示系统中该资源的数量。

信号量的值>0,表示有资源可以用, 可以申请到资源,

信号量的值<=0, 表示没有资源可以用, 无法申请到资源, 阻塞

信号量:还是一个受保护的变量,只能通过三种操作来访问: 初始化、P操作(申请资源)、V操作(释放资源)

sem_init: 信号量初始化

sem_wait: 申请资源,P操作,如果没有资源可以用则阻塞,否则就申请到资源 -1

sem_post: 释放资源,V操作,非阻塞 +1

3.3 函数接口

int  sem_init(sem_t *sem,  int pshared,  unsigned int value)  
功能:初始化信号量   
参数:sem:初始化的信号量对象
    pshared:信号量共享的范围(0: 线程间使用   非0:1进程间使用)
    value:信号量初值
返回值:成功 0
       失败 -1

int  sem_wait(sem_t *sem)  
功能:申请资源  P操作 
参数:sem:信号量对象
返回值:成功 0
       失败 -1
注:此函数执行过程,当信号量的值大于0时,表示有资源可以用,则继续执行,同时对信号量减1;当信号量的值等于0时,表示没有资源可以使用,函数阻塞

int  sem_post(sem_t *sem)   
功能:释放资源  V操作      
参数:sem:信号量对象
返回值:成功 0
      失败 -1
注:释放一次信号量的值加1,函数不阻塞

4.互斥

4.1 概念

互斥:多个线程在访问临界资源时,同一时间只能一个线程访问。

临界资源:一次仅允许一个线程所使用的资源

临界区:指的是一个访问共享资源的程序片段

互斥锁(mutex)通过互斥锁可以实现互斥机制,主要用来保护临界资源,每个临界资源都由一个互斥锁来保护,线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。

pthread_mutex_init

pthread_mutex_lock

pthread_mutex_unlock

pthread_mutex_destroy

4.2 函数接口

int  pthread_mutex_init(pthread_mutex_t  *mutex, pthread_mutexattr_t *attr)  
功能:初始化互斥锁  
参数:mutex:互斥锁
    attr:  互斥锁属性  //  NULL表示缺省属性
返回值:成功 0
      失败 -1

int  pthread_mutex_lock(pthread_mutex_t *mutex)   
功能:申请互斥锁     
参数:mutex:互斥锁
返回值:成功 0
      失败 -1
注:和pthread_mutex_trylock区别:pthread_mutex_lock是阻塞的;pthread_mutex_trylock不阻塞,如果申请不到锁会立刻返回

int  pthread_mutex_unlock(pthread_mutex_t *mutex)   
功能:释放互斥锁     
参数:mutex:互斥锁
返回值:成功 0
      失败 -1

int  pthread_mutex_destroy(pthread_mutex_t  *mutex)  
功能:销毁互斥锁     
参数:mutex:互斥锁


例如:打印倒置数组功能

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

int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
pthread_mutex_t lock;

void *handler_swap(void *arg)
{
    int t;
    while (1)
    {
        pthread_mutex_lock(&lock); // 申请互斥锁,申请不到就阻塞
        for (int i = 0; i < 5; i++)
        {
            t = a[i];
            a[i] = a[9 - i];
            a[9 - i] = t;
        }
        pthread_mutex_unlock(&lock); // 释放互斥锁
    }
    return NULL;
}

void *handler_print(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&lock);
        for (int i = 0; i < 10; i++)
            printf("%d ", a[i]);
        printf("\n");
        pthread_mutex_unlock(&lock);
        sleep(1); // 锁里面不要有耗时大的操作
    }
    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t tid1, tid2;
    // 初始化互斥锁
    if (pthread_mutex_init(&lock, NULL) != 0)
    {
        perror("init err");
        return -1;
    }

    pthread_create(&tid1, NULL, handler_swap, NULL);
    pthread_create(&tid2, NULL, handler_print, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

补充:死锁

是指两个或两个以上的进程或线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

死锁产生的四个必要条件

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

  1. 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

注意:当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

总结同步和互斥

互斥:两个线程之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行。

同步:两个线程之间也不可以同时运行,但他是必须要按照某种次序来运行相应的线程(也可以说是一种互斥)!

所以说:同步是一种更为复杂的互斥,而互斥是一种特殊的同步。

5.条件变量

5.1 概念

条件变量(cond)用于在线程之间传递信号,以便某些线程可以等待某些条件发生。当某些条件发生时,条件变量会发出信号,使等待该条件的线程可以恢复执行。

5.2 函数接口

一般和互斥锁搭配使用,来实现同步机制:

pthread_cond_init(&cond,NULL); //初始化条件变量

使用前需要上锁:

pthread_mutex_lock(&lock); //上锁

一些逻辑

pthread_cond_wait(&cond, &lock); //阻塞等待条件产生,没有条件产生时阻塞,同时解锁; 当条件产生时结束阻塞,再次上锁。

执行线程里面的逻辑

pthread_mutex_unlock(&lock); //解锁

pthread_cond_signal(&cond); //产生条件,不阻塞

pthread_cond_destroy(&cond); //销毁条件变量

注意:必须保证让pthread_cond_wait先执行,pthread_cond_signal再产生。

例如:打印和转置数组实现同步

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

int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
pthread_mutex_t lock;
pthread_cond_t cond;

void *handler_swap(void *arg)
{
    int t;
    while (1)
    {
        sleep(2); //保证pthread_cond_wait先执行
        pthread_mutex_lock(&lock); // 申请互斥锁,申请不到就阻塞
        for (int i = 0; i < 5; i++)
        {
            t = a[i];
            a[i] = a[9 - i];
            a[9 - i] = t;
        }
        pthread_cond_signal(&cond); //产生条件
        pthread_mutex_unlock(&lock); // 释放互斥锁
    }
    return NULL;
}

void *handler_print(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&lock);
        pthread_cond_wait(&cond,&lock); //没有条件产生时阻塞并解锁,有条件产生时则结束阻塞并再次上锁
        for (int i = 0; i < 10; i++)
            printf("%d ", a[i]);
        printf("\n");
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t tid1, tid2;
    // 初始化互斥锁
    if (pthread_mutex_init(&lock, NULL) != 0)
    {
        perror("init mutex err");
        return -1;
    }

    // 初始化条件变量
    if (pthread_cond_init(&cond, NULL) != 0)
    {
        perror("init cond err");
        return -1;
    }

    pthread_create(&tid1, NULL, handler_swap, NULL);
    pthread_create(&tid2, NULL, handler_print, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    pthread_mutex_destroy(&lock);
    pthread_cond_destroy(&cond);

    return 0;
}

Linux IO 模型

阻塞IO

非阻塞IO

信号驱动IO(了解)

IO多路复用

场景假设一

假设妈妈有一个宝宝,宝宝在房间里睡觉,妈妈需要及时知道宝宝是否醒了,如何做?

  1. 妈妈呆在房间,和宝宝一起睡觉: 妈妈不累,但是干不了其他事情。
  2. 妈妈时不时的看一下宝宝,其他时间可以干一点别的事情:妈妈累,但是可以干其他事情。
  3. 妈妈在客厅干别的,听宝宝是否哭了: 二者互不耽误。

1.阻塞IO:最常见、效率低、不浪费CPU

阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。

学习的读写函数在调用过程中会发生阻塞,相关函数如下:

•读操作中的read

读阻塞--> 需要读缓冲区中有数据可读,读阻塞解除

•写操作中的write

写阻塞--> 阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。

2.非阻塞式IO:轮询、耗费CPU、可以同时处理多路IO

•当我们设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”

•当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。

•应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。

•这种模式使用中不普遍。

2.1 通过函数自带参数设置

2.2 通过设置文件描述符属性为非阻塞

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ ); //file control
功能:设置文件描述符属性
参数:
   fd:文件描述符
   cmd:设置方式 - 功能选择
        F_GETFL  获取文件描述符的状态信息     第三个参数化忽略
        F_SETFL  设置文件描述符的状态信息     通过第三个参数设置
        O_NONBLOCK  非阻塞
        O_ASYNC     异步
        O_SYNC      同步
  arg:设置的值  in
返回值:
      特殊选择返回特殊值 - F_GETFL  返回的状态值(int)
      其他:成功0  失败-1,更新errno
0-原本: 阻塞、读权限    修改或添加为非阻塞
int flags=fcntl(0,F_GETFL); //1.获取文件描述符原有的属性信息
flags |= O_NONBLOCK;         //2.修改或添加属性为非阻塞
fcntl(0,F_SETFL,flags);      //3.设置修改后的属性信息

例子:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char const *argv[])
{
    // 1.获取文件属性信息
    int flags = fcntl(0, F_GETFL);
    // 2.修改或添加文件描述符属性为非阻塞
    flags |= O_NONBLOCK;
    // 3.设置文件描述符属性信息
    fcntl(0, F_SETFL, flags);

    char buf[32] = "";
    while (1)
    {
        fgets(buf, 32, stdin);
        printf("%s\n", buf);
        memset(buf, 0, 32);
        printf("############\n");
        sleep(1);
    }

    return 0;
}

注意:恢复阻塞模式需要关闭终端,或者设置回去。

flags &= ~O_NONBLOCK;

fcntl(0, F_SETFL, flags);

3.信号驱动IO:异步通知方式,需要底层驱动的支持

查看鼠标设备文件是哪个:

信号驱动I/O是一种异步I/O模型,通过操作系统向应用程序发送信号来通知数据可读或可写,从而避免轮询或阻塞等待。

异步通知:异步通知是一种非阻塞的通知机制,发送方发送通知后不需要等待接收方的响应或确认。通知发送后,发送方可以继续执行其他操作,而无需等待接收方处理通知。

  1. 通过信号方式,当内核检测到设备数据后,主动给应用发送信号SIGIO
  2. 应用程序收到信号后做异步处理即可。
  3. 应用程序需要把自己的进程号告诉内核,并打开异步通知方式。
//1.设置文件描述属性为将文件描述符和进程号提交给内核驱动
fcntl(fd,F_SETOWN,getpid());
//2. 设置异步通知方式
int flags=fcntl(fd,F_GETFL);
flags |= O_ASYNC;
fcntl(fd,F_SETFL,flags);
//3. signal捕捉SIGIO信号(内核会通知进程有新的IO信号可用)
//一旦内核给进程发送SIGIO信号则执行相应函数
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
int fd;
void handler(int sig)
{
    char buf[32];
    read(fd, buf, sizeof(buf));
    printf("mouse:%s\n", buf);
}

int main(int argc, char const *argv[])
{
    fd = open("/dev/input/mouse0", O_RDONLY);
    if (fd < 0)
    {
        perror("open err");
        return -1;
    }
    // 1.设置将文件描述符和进程号提交给内核驱动
    // 一旦fd有事件响应, 则内核驱动会给进程号发送一个SIGIO的信号
    fcntl(fd, __F_SETOWN, getpid());

    // 2.设置异步通知
    int flags;
    flags = fcntl(fd, F_GETFL); // 获取原属性
    flags |= O_ASYNC;           // 给flags设置异步   O_ASYNC 通知
    fcntl(fd, F_SETFL, flags);  // 修改的属性设置进去,此时fd属于异步

    // 3.signal捕捉SIGIO信号 --- SIGIO:内核通知会进程有新的IO信号可用
    // 一旦内核给进程发送sigio信号,则执行handler
    signal(SIGIO, handler);
    while (1)
    {
        printf("玩一玩\n");
        sleep(1); // SIGIO会让sleep函数直接返回
    }
    return 0;
}

阻塞IO(Blocking IO)

非阻塞IO(Non-blocking IO)

信号驱动IO(Signal-driven IO)

同步性

同步

同步

异步

描述

调用IO操作的线程会被阻塞,直到操作完成

调用IO操作时,如果不能立即完成操作,会立即返回,线程可以继续执行其他操作

当IO操作可以进行时,内核会发送信号通知进程

特点

最常见、效率低、不耗费cpu,

轮询、耗费CPU,可以处理多路IO,效率高

异步通知方式,需要底层驱动的支持

适应场景

小规模IO操作,对性能要求不高

高并发网络服务器,减少线程阻塞时间

实时性要求高的应用,避免轮询开销

场景假设二

假设妈妈有三个孩子,分别在不同的房间睡觉,需要及时获知每个孩子是否醒了,如何做?

阻塞IO? 在一个房间,不行

非阻塞IO? 不停的每个房间查看,可以但是累妈妈

信号驱动IO? 不行,因为只有一个信号,不知道哪个孩子醒了

方案:

  1. 不停的每个房间查看:超级无敌累!但是可以干点其他事情
  2. 妈妈在客厅睡觉,孩子醒了之后让保姆抱着去找妈妈:既可以休息,也可以及时获取孩子状态

4.IO多路复用: select/poll/epoll

  • 应用程序中同时处理多路输入输出流,若采用阻塞模式,得不到预期的目的;

● 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;

● 若设置多个进程/线程,分别处理一条数据通路,将新产生进程/线程间的同步与通信问题,使程序变得更加复杂;

● 比较好的方法是使用I/O多路复用技术。其基本思想是:

○ 先构造一张有关描述符的表(最大1024),然后调用一个函数。

○ 当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。

○ 函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。

4.1 select

特点

  1. 一个进程最多只能监听1024个文件描述符
  2. select被唤醒之后要重新轮询,效率相对低
  3. select每次都会清空未发生响应的文件描述符,每次拷贝都需要从用户空间到内核空间,效率低,开销大

编程步骤

  1. 先构造一张关于文件描述符的表
  2. 清空表 FD_ZERO
  3. 将关心的文件描述符添加到表中 FD_SET
  4. 调用select函数
  5. 判断是哪一个或者哪些文件描述符产生了事件 FD_ISSET
  6. 做对应的逻辑处理

函数接口

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
功能:
	实现IO的多路复用
参数:
	nfds:关注的最大的文件描述符+1
   readfds:关注的读表
	writefds:关注的写表 
	exceptfds:关注的异常表
	timeout:超时的设置
		NULL:一直阻塞,直到有文件描述符就绪或出错
		时间值为0:仅仅检测文件描述符集的状态,然后立即返回
		时间值不为0:在指定时间内,如果没有事件发生,则超时返回0,并清空设置的时间值

struct timeval {
    long tv_sec;		/* 秒 */
    long tv_usec;	/* 微秒 = 10^-6秒 */
};

返回值:
	成功时返回准备好的文件描述符的个数
	0:超时检测时间到并且没有文件描述符准备好	
    -1 :失败
注意:
	select返回后,关注列表中只存在准备好的文件描述符

操作表:
void FD_CLR(int fd, fd_set *set); //清除集合中的fd位
void FD_SET(int fd, fd_set *set);//将fd放入关注列表中
int  FD_ISSET(int fd, fd_set *set);//判断fd是否产生操作 是:1 不是:0
void FD_ZERO(fd_set *set);//清空关注列表

例子:处理两路IO分别是鼠标和键盘

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

int main(int argc, char const *argv[])
{
    // 打开鼠标文件描述符
    int fd_mouse = open("/dev/input/mouse0", O_RDONLY);
    if (fd_mouse < 0)
    {
        perror("open err");
        return -1;
    }
    printf("fd_mouse:%d\n", fd_mouse);

    // 1.创建表 利用select实现 0:键盘 fd_mouse:鼠标
    fd_set readfds;

    while (1)  //每次调用select之后都会把没发生响应的文件描述符在表中清空,所以每次循环都要重新构建一下这个表类似于初始化一下表。
    {
        // 2.清空表 FD_ZERO
        FD_ZERO(&readfds);

        // 3.添加关心的文件描述符到表中 FD_SET
        FD_SET(0, &readfds);
        FD_SET(fd_mouse, &readfds);

        // 4.调用select函数进行监听表中是否有描述符产生操作
        int maxfd = fd_mouse;

        if (select(maxfd + 1, &readfds, NULL, NULL, NULL) < 0)
        {
            perror("select err");
            return -1;
        }
        // 5.判断是哪个文件描述符产生了操作 FD_ISSET
        char buf[64] = "";
        if (FD_ISSET(0, &readfds))
        {
            // 6.做对应的逻辑处理
            scanf("%s", buf);
            printf("keybroad:%s\n", buf);
        }

        if (FD_ISSET(fd_mouse, &readfds))
        {
            ssize_t n = read(fd_mouse, buf, sizeof(buf) - 1); // 一定预留一个元素给\0
            buf[n] = '\0';                                    // 以为read不会自动补\0,所以可以手动添加一个\0作为结尾
            printf("mouse:%s\n", buf);
        }
    }

    close(fd_mouse);

    return 0;
}

超时检测

概念

什么是网络超时检测呢,比如某些设备的规定,发送请求数据后,如果多长时间后没有收到来自设备的回复,那么需要做出一些特殊的处理

比如: 链接wifi的时候,等了好长时间也没有连接上,此时系统会发送一个消息: 网络连接失败;

必要性
  1. 避免进程在没有数据时无限制的阻塞;
  2. 规定时间未完成语句应有的功能,则会执行相关功能

4.2 poll

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:同select相同实现IO的多路复用
参数:
    fds:指向一个结构体数组的指针,用于指定测试某个给定的文件描述符的条件。
    nfds:指定的第一个参数数组的元素个数。
    
    timeout:超时设置 
        -1:永远等待 
         0:立即返回 
        >0:等待指定的毫秒数
        
    struct pollfd {
        int fd; // 文件描述符
        short events; // 等待的事件
        short revents; // 实际发生的事件
    };
返回值:
    成功时返回结构体中 revents 域不为 0 的文件描述符个数
    0: 超时前没有任何事件发生时,返回 0
    -1:失败并设置 errno

特点:

  1. 优化文件描述符的限制,文件描述符的限制取决于系统
  2. poll被唤醒之后要重新轮询一遍,效率相对低
  3. poll不需要重新构造表,采用结构体数组,每次都需要从用户空间拷贝到内核空间

实现过程

  1. 创建一张表,也就是一个结构体数组struct pollfd fds[100];
  2. 添加关心的描述符到表中
  3. 循环poll监听更新表
  4. 逻辑判断

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

int main(int argc, char const *argv[])
{
    int fd = open("/dev/input/mouse0", O_RDONLY);
    if (fd < 0)
    {
        perror("open err");
        return -1;
    }
    //1.创建表也就是结构体数组
    struct pollfd fds[2];

    //2. 将关心的文件描述符添加到表中,并赋予事件
    fds[0].fd = 0;          //键盘
    fds[0].events = POLLIN; //想要发生的事件是读事件
    
    fds[1].fd = fd;         //鼠标
    fds[1].events = POLLIN;

    //3.保存数组内最后一个有效元素下标
    int last = 1;

    //4.循环调用poll监听
    while (1)
    {
        int ret = poll(fds, last + 1, 2000);
        if (ret < 0)
        {
            perror("poll err");
            return -1;
        }
        else if (ret == 0)
        {
            printf("time out\n");
            continue;
        }

        //5.判断结构体内文件描述符实际发生的事件
        char buf[32] = "";
        //键盘
        if (fds[0].revents == POLLIN)
        {
            //6.根据不同的文件描述符发生的不同事件做对应的逻辑处理
            fgets(buf, sizeof(buf), stdin);
            printf("keyboard: %s\n", buf);
        }
        //鼠标
        if (fds[1].revents == POLLIN)
        {
            ssize_t n = read(fd, buf, sizeof(buf) - 1);
            buf[n] = '\0';
            printf("mouse: %s\n", buf);
        }
    }
    close(fd);
    return 0;
}

4.3 epoll

特点:

  1. 监听的最大的文件描述符没有个数限制
  2. 异步IO,epoll当有事件产生被唤醒之后,文件描述符主动调用callback函数(回调函数)直接拿到唤醒的文件描述符,不需要轮询,效率高
  3. epoll不需要重新构造文件描述符表,只需要从用户空间拷贝到内核空间一次。

总结

select

poll

epoll

监听个数

一个进程最多监听1024个文件描述符

由程序员自己决定

百万级

方式

每次都会被唤醒,都需要重新轮询

每次都会被唤醒,都需要重新轮询

红黑树内callback自动回调,不需要轮询

效率

文件描述符数目越多,轮询越多,效率越低

文件描述符数目越多,轮询越多,效率越低

不轮询,效率高

原理

每次使用select后,都会清空表

每次调用select,都需要拷贝用户空间的表到内核空间

内核空间负责轮询监视表内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用select,如此循环

不会清空结构体数组

每次调用poll,都需要拷贝用户空间的结构体到内核空间

内核空间负责轮询监视结构体数组内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用poll,如此循环

不会清空表

epoll中每个fd只会从用户空间到内核空间只拷贝一次(上树时)

通过epoll_ctl将文件描述符交给内核监管,一旦fd就绪,内核就会采用callback的回调机制来激活该fd,epoll_wait便可以收到通知(内核空间到用户空间的拷贝

特点

一个进程最多能监听1024个文件描述符

select每次被唤醒,都要重新轮询表,效率低

select每次都清空未发生相应的文件描述符,每次都要拷贝用户空间的表到内核空间

优化文件描述符的个数限制

poll每次被唤醒,都要重新轮询,效率比较低(耗费cpu)

poll不需要构造文件描述符表(也不需要清空表),采用结构体数组,每次也需要从用户空间拷贝到内核空间

监听的文件描述符没有个数限制(取决于自己的系统)

异步IO,epoll当有事件产生被唤醒,文件描述符会主动调用callback函数拿到唤醒的文件描述符,不需要轮询,效率高

epoll不需要构造文件描述符的表,只需要从用户空间拷贝到内核空间一次。

结构

数组

数组

红黑树+就绪链表

开发复杂度

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐