【Linux系统编程:进程间通信】匿名管道 pipe | 命名管道 fifo | system V --- 共享内存 shmget shmctl shmat shmdt,消息队列,信号量

本文主要是介绍【Linux系统编程:进程间通信】匿名管道 pipe | 命名管道 fifo | system V --- 共享内存 shmget shmctl shmat shmdt,消息队列,信号量,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

写在前面

在日常生活中,通信的本质是传递信息,但具体在程序员角度,通信的本质是传递数据。
进程间通信就是进程之间互相传递数据,那么进程间能直接相互传递数据吗?—— 不能,因为进程具有独立性,所有的数据操作,都会发生写时拷贝。父子进程都不能传递,更别谈两个进程毫无关系。
所以两个进程要通信一定要通过中间媒介的方式来进行通信,所以必须先想办法让不同的进程看到同一份公共的资源,这里所谓的公共的资源就是系统通过某种方式提供的系统内存。 这块空间通常是由操作系统提供的,可以被两块不同的进程看到,然后它们才能实现通信。传递数据就是由一个进程拷到对应的内存里,这块内存另一个进程当然也能看到,所以也自然能从内存里拷到自己的进程中。
综上,就知道了进程间通信要学的就是如何通过系统,让不同的进程看到同一份资源。操作系统提供的通信方案有很多种,这句话的含义就是操作系统让不同进程看到同一份资源的方式有很多种,最典型的有管道、消息队列、共享内存、信号量等等。这里主要谈管道、共享内存,信号量会在多线程部分展开,这个部分主要以概念为主。
所以进程间通信的本质就是让不同的进程,能看到同一份系统资源,而这份资源就是系统通过某种方式提供的系统内存,因为方式是有差别的,所以通信策略也是有差别的。

一、进程间通信

💦 进程间通信的目的
  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源
  • 通知事件:一个进程需要向另一个或另一组进程发送消息,通知它 (它们) 发生了某种事件 (如进程终止时要通知父进程)
  • 进程控制:有些进程希望完全控制另一个进程的执行 (如 Debug 进程),此时控制进程希望能够拦截另一进程的所有陷入和异常,并能够及时知道它的状态改变
💦 进程间通信的发展

这里简单来说在进程间通信发展的过程主要有两种流派,一种是只在主机上通信,就是 System V,另一种是可以在主机上的进程跨网络通信,就是 POSIX。本章主要学习 System V,网络部分再学习 POSIX。

管道是操作系统本身提供的,所以本章能接触到的是管道和 System V 进程间的通信方式。

  • 管道
  • System V 进程间通信
  • POSIX 进程间通信
💦 进程间通信的分类
  • 管道

    1. 匿名管道
    2. 命名管道
  • System V IPC

    1. System V 消息队列
    2. System V 共享内存
    3. System V 信号量
  • POSIX IPC

    1. 消息队列
    2. 共享内存
    3. 信号量
    4. 互斥量
    5. 条件变量
    6. 读写锁

二、管道

💦 什么是管道

现实中也存在着很多管道,它们一般都存在着一个入口和一个出口,其中就传送着人们所需要的水资源等;而互联网中的管道传送的是数据资源,所以计算机就模拟出一条管道,而数据资源一定是有人想传,且有人想收,这里的两个有人对应发送进程和接受进程。
现实中构建管道所使用的材料是钢铁,而计算机中构建管道缓冲区所使用的材料是系统内存,而这里的系统内存就是让不同进程所看到的同一块系统资源。上面所说的概念是一种感性的理解,还没有涉及到任何的系统概念,归根结底是想让大家明白不同角色的定位。

  • 管道是 Unix 中最古老的进程间通信形式
  • 我们把从一个进程连接到另一个进程的数据流称为一个管道

管道一共有两种通信方案,匿名管道和命名管道,它们的底层原理基本是一样的,区别是它们各自的侧重点。

💦 匿名管道 pipe

匿名管道是供具有血缘关系的进程,进行进程间通信,常见于父子,即便是父子,它们的数据也不是共享的,而是私有的,凡是共享的是因为双方都不写入罢了。所有的通信方式,尤其是进程间通信,第一是先保证不同的进程看到同一份资源。匿名管道就是这个管道没有名字,它也不需要,匿名管道是由子进程继承父进程的文件描述符中的内容来的。

  • 怎么保证父子进程能看到同一份资源呢

    对于文件描述符我们当然很熟悉,这当然是和管道强相关的,这里只想强调的是在 struct file 之后是提供文件的方法和缓冲区的。

    在这里插入图片描述

    管道的原理就是先让父进程以读和写的方式打开同一个文件,可以理解成以读方式打开一次,再以写方式打开一次 (注意这里只是为了好理解才这样表述,实际上创建管道,它有自己独立的接口)。相当于就是父进程以读又以写打开一个 pipe_file 文件 (把同一个文件打开两次就得到不同的文件描述符,比如说默认的 1 和 2,所以对于一个文件来说当然可以以读的方式打开一次又以写的方式打开一次,不过一般是读方式或写方式其中一种,即便同时打开,用的也是同一个接口,而今天要讲原理,就自然不是按使用的方式来看待),这个过程就称为创建管道的过程。
    所以管道有了,就需要通信数据,所以父进程 fork 子进程 (这里还要强调一下,子进程是一个独立的进程,有自己独立的地址空间、页表、文件描述符表,代码共享,数据各自私有,但是结构中的大部分数据是以父进程为模板),所以子进程文件描述符表中写入的内容和父进程是一样的,最重要的是曾经父进程打开的 pipe_file 文件,现在子进程中的 3 号 4 号文件描述符也指向 pipe_file 文件。
    这就是进程间通信的第一步,保证不同的进程,看到同一份资源,而这份资源就是系统提供的一段内存区域,现在就可以理解父进程通过 3 或 4 号文件描述符往管道中对应读写的数据就在这个文件对应的缓冲区中,而子进程当然可以通过 3 或 4 号文件描述符往管道中也读写数据。
    最后这里想说对于地址空间、文件描述符表等数据结构虽然父子进程不共享,但是文件描述符表中的内容是一样的,也就意味着父子进程可以指向同一份文件。管道本质也是一个文件,只不过这个文件不会在磁盘上持久保存。

    在这里插入图片描述

  • 管道只能进行单向数据通信

    这就意味着,要么是父进程写,子进程读,要么是子进程写,父进程读。总之一个管道只能进行单身数据通信,若要双向通信,就只能建立多个管道。

    如果想让父进程写,子进程读,就关闭父进程的读,子进程的写;如果想让子进程写,父进程读,就关闭子进程的读,父进程的写;父子进程关闭不需要的文件描述符,这样就可以达到构建单向通信信道的目的。

    在这里插入图片描述

    构建单向信道时父子进程最后都要关闭一个文件描述符,为什么曾经还要打开呢 ❓
    根本原因是若父进程只以读或只以写的方式打开这个文件,那么 fork 后子进程仅仅只有对应的读或者只有写,这样就会造成父子进程要么都是读,要么都是写,这样就不能完成管道的单向通信。
    还有一个原因是需要灵活的控制父子进程来完成读写通信,所以最终是父进程写,子进程读;还是子进程写,父进程读。这完全取决于你的场景。

    对应的一组写和读可以不关闭吗 ❓
    这样也没错,这一组放着就放着,不用也可以达到管道的单向通信。不过一般建议要关闭,因为一方面证明了管道的单向通信这样的特性,另一方面主要是为了防止误操作。当然我们也并不确定各种操作系统对于管道的支持情况,所以最好按照标准规范。

    为什么管道在设计时只支持单向通信 ❓
    这是与文件系统强相关的,如果能设计双向通信人家早就这样设计了。不能的原因大概率与文件的读写位置有关系,一个文件的读写位置只有一个,如果要实现管道双向通信,就一定要让双方既能读又能写,所以读写位置必须是两对,所以需要修改文件系统,这样大可不必,直接创建两个管道即可。

  • 注意并不是所有文件都可以当作管道的,但是管道确实是一种文件。比如 touch 一个 log.txt,然后两毫不相关的进程一个以写方式打开,另一个以读方式打开。这样是比较困难的,虽然两进程能看到同一个文件,但那样需要写进程把数据刷新到磁盘,读进程再从磁盘读取,这并不是系统想支持的通信方案。通信一定要考虑成熟、稳定且高效。

  • pipe

    在这里插入图片描述

    pipe 是我们要认识的一 个创建匿名管道的系统调用接口。
    pipe 的参数是一个具有 2 个参数的数组,大家都知道数组传参会降维成指针。这里 pipe 的参数是一个输出型参数,这种参数已经不是第一次接触了,说白了就是我不想给你传入什么,而是想调用你然后再拿回什么。我们可以通过这个参数拿到打开的管道文件的 fd,这个数组有两个参数,这意味着它会拿到 2 个 fd —— read、write。不妨思考一下它在底层无非就是让父进程以读方式和以写方式分别打开一个文件,然后得到两个文件描述符。据经验判断,我们默认会拿到的 fd 是 3 和 4。

    1. 父进程创建管道

      在这里插入图片描述

    2. 父进程 fork 子进程

      在这里插入图片描述

    3. 子进程写,父进程读,通常 fd[0] 对应 read,fd[1] 对应 write,子进程关闭 fd[0],父进程关闭 fd[1],再让父进程等待子进程

      在这里插入图片描述

    4. 父子进程实现通信

      在这里插入图片描述

      1. \0 是 C 语言中的规定,不是文件的规定。所以子进程在 write 时不要写 \0

      2. \0 不是文件的规定,而是 C 语言的规定,所以父进程在往 buffer 里读入数据的时候,需要预留一个位置给 \0

      3. 必然不可能把 \0 写入文件中,也不可能从文件中读取 \0

      4. read 的返回值

        在这里插入图片描述

        read 成功,返回它读到了多少个字节,0 表示读到文件结尾,-1 表示出错。写端不仅仅写,在写完后还把写的文件描述符关闭,此时另一端再读就会读到 0。若返回值大于 0,则读取成功,并追加 \0。若返回值等于 0,则子进程不再继续写入了,子进程关闭写文件描述符,并退出。若其它情况,则 read 失败了,这里暂且不做处理。此时子进程不断的往管道写入数据,父进程不断的往管道读入数据到 buffer,并打印,每次循环都把 buffer 中的内容清空,以验证父进程的打印数据一定是从子进程中来的 (读端从管道中成功读取数据之后,管道中的数据就会被置为无 效,下次再写就会覆盖,后面会讲生产者消费模型)。

      5. 父子进程在通信完后建议要关闭文件对应的文件描述符

      6. 父进程写,子进程读当然也可以,下面会解释为什么不那么做

  • 特性

    1. 管道自带同步机制

      子进程写完数据后休眠一秒,而父进程没有,父进程一瞬间就读完数据,在子进程休眠的那一秒之内,父进程在干什么呢 ❓

      此时管道中没有数据,父进程在进行等待管道内部有数据就绪。这需要写端造成。

      此时子进程疯狂往管道里写,而父进程休眠 100 秒 ❓

      在这里插入图片描述
      在这里插入图片描述

      子进程写到 2720 次时就没写了。换言之,如果管道里写端已经写满了,此时是不能再继续写入了,而写端就在等待管道内部有空闲空间。这需要读端造成。

      综上两问,在管道中,通信双方:一方不写了,另一方把数据读完后就必须等待对方写入才可以继续读;反之一方写满了,另一方不读,一方就必须等待另一方读取后才可以继续写。这种特性叫做进程间同步,它们两个必须得通过某种同步机制来保证数据安全:管道是内存空间,如果不写,你还在那读,读到的数据一定是垃圾数据;同样,如果一直写,你不读,就有可能会覆盖原来的数据。
      它其实是一种保护临界资源的一种处理方案,后面会细谈。

    2. 管道是单向通信

    3. 管道是面向字节流

      流 ❓

      这里简单理解一下就好,真正要理解需要学习网络。这是一段缓冲区,一定有人去缓冲区中写入和读取。流就是想按几个字节就按几个字节写,想按几个字节读就按几个字节读。像这样的缓冲区对于读和写来说就是字节流。

      在这里插入图片描述

    4. 管道只能保证具有血缘关系的进程通信,常用父子

    5. 管道可以保证一定程度的数据读取的原子性

      若往管道写 hello world,刚准备写 world,而 hello 就被读走了,此时就不能保证原子性。这里的一定程度一般指的是 4kb。

完整代码

#include<stdio.h>
#include<string.h>
#include<stdlib.h.>
#include<unistd.h>
#include<sys/wait.h>int main()
{int pipe_fd[2] = {0};if(pipe(pipe_fd) < 0){perror("pipe");return 1;}printf("%d, %d\n", pipe_fd[0], pipe_fd[1]));pid_t id = fork();if(id < 0){perror("fork");return 2;}else if(id == 0){//child - writeclose(pipe_fd[0]);const char* msg = "Hello father, I am child";int count = 5;while(count){write(pipe_fd[1], msg, strlen(msg));//sizeof(msg) -> 8byte //pipd_fd[1] = 4sleep(1);count--;}close(pipe_fd[1]);exit(0);}	else{//father - readclose(pipe_fd[1])char buffer[64];while(1){buffer[0] = 0;ssize_t size = read(pipe_fd[0], buffer, sizeof(buffer) - 1);if(size > 0){buffer[size] = 0;printf("father get massage from child# %s\n", buffer);}else if(size == 0){printf("pipe file close, child quit!\n");break;	}else{//TODO - errbreak;}}int status = 0;if(waitpid(id, &status, 0) > 0){printf("child quit, wait success!\n");	}close(pipe_fd[0]);}return 0;	
}

请添加图片描述

  • 验证部分特性

    若读取关闭,一直写,肯定是没意义的,其本质就是在浪费资源,所以此时写进程会立马被 OS 通过发送信号的方式终止,而写进程是子进程,此时父进程就可以 waitpid,从而知道子进程退出的原因。可以看到唯一还没有研究的正是 write 端写和 read 端不读&关闭,这也就是为什么要让子进程写,父进程读的原因,因为它更适合测试。

    在这里插入图片描述

    此时子进程不断的写,而父进程读取一次后就关闭读,子进程再写,OS 就发送 13 号信号终止了进程。

    在这里插入图片描述


    要测试管道有多大这里有两种方案:

    1. 子进程一直写,每次写一个字节,然后计数器统计,父进程不要读。可以看到结果是 65536byte,也就是管道的大小是 64kb,当然这是跟操作系统有关。

      在这里插入图片描述

    2. ulimit -a 查看系统资源

      这里算下来也才 4 kb,而实践出来又是 64kb。这里的 64kb 是当前云服务器管道的最大容量;而这里的 4kb 是以原子性写入管道中的单元大小 (这里可以 man 7 pipe 手册查看,它说 PIPE_BUF 是 4096byte(4kb),只要在这个范围以内都是原子的)。

      在这里插入图片描述

      在这里插入图片描述


在这里插入图片描述

sleep 在系统中也是一条命令,这里就是想让这三个 sleep 进程别立马退出,然后让它们 & 在后台运行。ps 后我们发现这三个进程是兄弟进程,而 10127 一定是 bash。系统就是 pipe 管道 1 和 管道 2,通过 for 循环 fork 三个进程 (如果是父进程就继续 fork),它们都能之前打开的两个管道文件,然后它们三再关闭对应的读写端形成一条单身的数据流。通过 |,这样就可以实现 sleep 1000 到 sleep 2000 或者其它命令之间进程间通信。换言之,我们曾经使用到的 | 就是匿名管道。

在这里插入图片描述


补充:进程退出,曾经打开的文件也会被关闭 (因为进程中保存着打开文件的相关数据结构,而进程退出后,文件就自然被关闭)。同样,管道也是文件,所以管道的生命周期就是进程的生命周期。

💦 命名管道 fifo

命名管道是供毫不相关的进程,进行进程间通信。命名管道一般叫做 fifo,fifo 一定不陌生,因为数据结构中队列就是这种特性,但这不重要。

  • 理解命名管道的原理

    要让两个毫不相干的进程进行通信,第一件事一定是先保证两进程能看到同一份资源,因为文件路径所以让进程 1 和 进程 2 分别以读写打开同一路径下的文件,此时内存中一定会包含 struct file 结构体,以及该文件对应的缓冲区。所以此时进程 2 把数据写到缓冲区中,进程 1 就可以进行读取。命名管道也是管道,它也遵守管道的面向字节流,同步机制,单向通信等特点。唯一和匿名管道不同的是它可以和不相关的进程进行通信。

    对于普通文件,是需要将数据刷新到磁盘,持久化存储,所以它就应该要把写入的数据刷新到磁盘,换言之,进程 2 把文件打开写数据到磁盘然后关闭,进程 1 再从磁盘读取。这当然可以通信,但是数据放在磁盘上效率太低了,所以便没有什么价值。所以系统中就存在一种特殊的文件 —— 管道文件,它虽然也有路径标识,但是系统不会把对应的内存数据刷新到磁盘。下面将从命令行和代码上完成通信测试。

    在这里插入图片描述

  • 命令行

    fifo 是一条命令

    在这里插入图片描述

    但是在命令行上创建命名管道要使用 mkfifo

    在这里插入图片描述

    1. mkfifo myfifo,此时 myfifo 就是一个管道文件,此时往文件中写入数据后,它的大小依旧是 0,因为数据只会在内存中,不会往磁盘刷。

      在这里插入图片描述

    2. 此时第一个命令行不断的往管道里写数据,而第二个命令行以管道作为标准输入然后输出重定向到 cat,最后显示出来 (或者可以说 cat 从管道中把数据读取出来),这就完成了两个进程之间的通信。

      请添加图片描述

  • 代码

    1. 准备工作

      想要 make 后一次生成两个不相关的可执行程序,需要在开头定义 all 伪目标,它依赖的是两个可执行程序,没有依赖方法 (因为它有依赖关系,所以 makefile 会推导 client 和 server 怎么形成)。这里在提一下,虽然 makefile 这样的技术已经很老了,但是它很稳定,它几乎是现在主流的各种各样的工具的基础,实际在公司也不会自己写 makefile (除非自己写测试代码),公司一般都有很多工具来自动生成 makefile,但是必要的 makefile 编写还是要了解的,因为上层的工具和 makefile 有关系。

      在这里插入图片描述

    2. mkfifo 函数

      没错 mkfifo 既是命令,也是一个库函数,第一个参数是命名管道的路径,第二个参数是命名管道的权限。成功返回 0,失败返回 -1。

      在这里插入图片描述

    3. 实现通信

      此时代码中的 mkfifo 和命令中的 mkfifo 达到的效果是一样的

      在这里插入图片描述

      此时要进行通信就很简单了,上面的命名管道的原理已经说过了。这里 client.c 中以写打开管道文件,然后从键盘读取数据到 buffer,然后在往管道中写入 buffer 中的数据。然后 server.c 以读打开管道文件,把数据往 buffer 中读,然后再打印 buffer 中的数据。

      server.c - read

      #include <stdio.h>
      #include <sys/types.h>
      #include <sys/stat.h>
      #include <fcntl.h>
      #include <unistd.h>#define FIFO "./fifo"int main()
      {	int ret = mkfifo(FIFO, 0644);if(ret < 0){perror("mkfifo");return 1;	}int fd = open(FIFO, O_RDONLY);if(fd < 0){perror("open");return 2;	}char buffer[128];while(1){buffer[0] = 0;	ssize_t s = read(fd, buffer, sizeof(buffer) - 1);if(s > 0){buffer[s] = 0;printf("client# %s\n", buffer);	}else if(s == 0){printf("client quit...\n");break;	}else{break;	}}close(fd);return 0;
      }
      

      client.c - write

      #include <stdio.h>
      #include <sys/types.h>
      #include <sys/stat.h>
      #include <fcntl.h>
      #include <unistd.h>#define FIFO "./fifo"int main()
      {	int fd = open(FIFO, O_WRONLY);if(fd < 0){perror("open");return 2;	}char buffer[128];while(1){printf("Please Enter# ");fflush(stdout);buffer[0] = 0;	ssize_t s = read(0, buffer, sizeof(buffer) - 1);if(s > 0){buffer[s] = 0;write(fd, buffer, strlen(buffer));}else if(s == 0){printf("client quit...\n");break;	}else{break;	}}close(fd);return 0;
      }
      

      结果

      请添加图片描述

💦 匿名管道对比命名管道
  1. 匿名管道是供具有血缘关系的进程进行进程间通信,命名管道可供非具有血缘关系的进程进行进程间通信。

  2. 匿名管道是通过父子共享文件的特征让进程看到同一份资源,而命名管道是文件路径具有唯一性的特征让进程看到同一份资源。

  3. pipe 创建的管道文件因为没有名字,所以它只在内存;fifo 创建的管道文件有名字,它当然在磁盘上,只不过不会把数据写到磁盘上。

三、system V ----- 共享内存

这里我们还要了解进程间通信之 system V 标准下的共享内存,上面所说的管道其实不属于 system V 标准,但是它依旧是操作系统下最原生的通信方式。system V 标准下有最典型的三种通信方式:共享内存、消息队列、信号量。本章重点谈共享内存,消息队列简单提一下,然号和信号量在多线程中在了解。

  • 共享内存原理

    正如上面两种管道,进程间通信的第一步一定是先让不同的进程看到同一份资源,然后才是通信的过程。进程间通信中的大部分内容都是第一步,在之前要让不同进程看到同一份资源:匿名管道是通过父子共享文件的特征;命名管道是通过文件路径具有唯一性。其中这两种管道归根结底所看到的资源都是文件资源。
    对于下图中的内容,我们很早就接触过了。操作系统为了满足通信需求,操作系统能够 1) 在物理内存上申请一块物理内存空间,2) 再把这块空间通过页表映射到共享区 (也就是堆栈之间),3) 最后将映射之后的虚拟地址返回给用户。操作系统当然可以做到这些操作,因为操作系统是软硬件资源的管理者,这也不难理解,在 C/C++ 中使用 malloc、new 时,实际上就是在堆上申请空间,最后也还是在物理内存中申请,然后返回地址。以前 malloc/new 是为了让你这个进程私有使用,而今天是为了让多个进程能看到同一份资源。
    此时又有一个进程 B,它和进程 A 没有任何关系,虽然它们是类似的数据结构来表示进程,但是它们的代码和数据是被加载到内存中不同的位置,所以实际上它们是具有很强的独立性。这个话题很早就说过了,当时还举了一个例子是一个全局变量,然后父进程直接打印地址和值,子进程修改内容后再打印地址和值,最后地址一样,值不一样。
    同样对于进程 B 操作系统也可以向物理内存申请空间,然后映射到共享区,返回给进程。不过我们要做到的是让不同进程看到同一份资源。所以原理就是操作系统向物理内存申请一块空间,这块空间就叫做共享内存,然后再把这块空间分别映射到两个进程中 mm_struct 的共享区 (这个共享区我们在基础IO 中说过动态库是被映射到这个区域的,现在就知道了物理内存中申请的共享内存也会映射到这块区域),然后返回给进程,那么这两个进程就可以使用各自的虚拟地址,页表,访问同一块物理内存,这就是共享内存。而上述步骤一定是有对应的系统调用接口帮助我们实现。
    操作系统内部是提供通信机制的(IPC),也就是其中有一个 ipc 模块。上面说操作系统申请一块内存空间,但也得是人来告诉操作系统自己需要申请,所以本质还是进程申请的。宏观来看,操作系统内一定存在大量的共享内存,所有的共享内存都是进程向操作系统申请的,其中操作系统当然要管理诸多共享内存,怎么管理呢?—— 先描述,再组织 (共享内存是给进程用的,而操作系统为了管理这些共享内存,它也要申请大量对应的数据结构来维护),所以操作系统对共享内存的管理,就变成了对共享内存所对应的数据结构的管理 (一会下面会演示共享内存所对应的数据结构)。
    所以流程就是 1) 申请共享内存 2) 进程 A 和进程 B 分别挂接对应的共享内存到自己的地址空间,也就是共享区 3) 双方就能看到同一份资源,也就可以通信了 4) 释放共享内存

    在这里插入图片描述

💦 shmget
  • 认识接口

    在这里插入图片描述

    shmget 是系统提供来申请共享内存的一个系统接口。

    1. size 是你想申请共享内存的大小,理论上是可以任意,但建议是 4kb 的倍数,一会解释。

    2. shmflg 有 IPC_CREAT 和 IPC_EXCL 两个选项。前者是创建共享内存,后者单独使用并没有意义。其次这里还可以 | 上一个八进制方案,表示这个共享内存的权限。

      若同时设置 IPC_CREAT 和 IPC_EXCL,那么目标共享内存不存在,则创建;否则,则出错返回。这样做的意义是如果调用 shmget 成功,一定得到的是全新的共享内存,因为它失败就出错了。所以一般这两个选项会组合使用,从而从 0 到 1 的创建一个共享内存。

      若只设置 IPC_CREAT (同 0),那么目标共享内存不存在,则创建;否则,则获取共享内存。

      一会一定是一个进程设置 IPC_CREAT | IPC_EXCL,另一个进程设置 IPC_CREAT (什么叫做同时设置呢,曾经我们就说过标志位用 int 太浪费,所以这里用的是一个 bit 位来表示一种状态,若有多个需要同时设置就用 |,这里可以验证一下,可以看到这里 define 的是一种 8 进程数据,这里 1 2 4 就说明了用的是一串 01 序列,但只有一个 1,且 1 的位置不一样,所以 | 就可以获取到多个标志位)。

      在这里插入图片描述

      如果共享内存已经存在了,此时就不应该再创建了,而是获取,因为很显然一个创建好共享内存的进程要与另一个进程通信的话,另一个进程就只能是获得要通信进程的对应的那个共享内存

    3. 返回值:若成功,它会返回一个合法的内存标识符;否则就返回 -1。它可以通过这个返回值来唯一标识这个共享内存。这个概念有点类似文件描述符,共享内存 ipc 机制也确实与文件系统有关,但 ipc 机制是操作系统另外一个独立的模块,这样的小模块还有很多,之前了解的都是一些宏观上的模块,就如进程管理、文件管理、内存管理、驱动管理。

    4. 怎么保证两进程看到的是同一个共享内存呢

      在这里插入图片描述

      每个共享内存都有自己对应的数据结构 struct shm_ipc,此时通过 key 就可以进行唯一区分 (就像身份证号码更多的是强调唯一性)。其中 A 进程创建了共享内存,key 值是 123,B 进程想要和 A 进程通信,就需要遍历共享内存数据结构中的 key 值。

      现在问题就变成了如何保证两进程获得的是同一个 key 值呢 —— ftok,它和 fork 没有任何关系,它没有任何的系统调用,它只是把第一参数的字符串和第二个参数的整数合起来形成一个唯一的 key 值。它可以按照自己的情况,任意填写,但必须保证要通信的两个进程填的是一样的,这样就可以保证两个进程使用的是同一个规则形成 key。

      在这里插入图片描述

  • 代码

    1. makefile

      makefile 中是可以定义变量的, makefile 中取变量用 $()。

      在这里插入图片描述

    2. common.h

      在这里插入图片描述

    3. 必须先保证 server.c 和 client.c 它们中 ftok 获取的 key 值是一样的,ftok 本身没有任何的系统调用,key 值就是 ftok 将 PATH_NAME 和 PROJ_ID 组合形成唯一的 key 值。

      在这里插入图片描述

      在这里插入图片描述

    4. 申请共享内存

      1. ipcs

        ipcs 命令默认它会查看 Message Queues (消息队列)、Shared Memory Segments (共享内存段)、Semaphore Arrays (信号量数组) 相关信息。若只想查看,如共享内存则 ipcs -m,这里可以看到好像没有啥共享内存,sudo 后也没有,没关系,我们马上打开一个共享内存。

        在这里插入图片描述

        此时 ./server,输出结果后,server 进程当然退出了,所以它的退出码是 0。

        在这里插入图片描述

        此时再 ipcs -m 就看到 server 进程所申请的共享内存信息了。

        在这里插入图片描述

        正如上面所看到的 server 进程已经结束了,但是它所申请的 ipc 共享内存资源仍然存在。这里想说的是所有的 ipc 资源都是随内核,不随进程。这里有两种方法可以释放共享内存:

        1. 进程退出时,用调用释放 (有申请共享内存,当然也有释放嘛,一会代码演示)

        2. 操作系统进行重启或者命令行指令 (ipcrm -m shmid 释放共享内存,规范应该是由所对应的进程来调用系统接口来释放的)

          在这里插入图片描述

💦 shmctl
  • 认识接口

    既然有 shmget 来申请共享内存,那么也必须要有 shmctl 来释放共享内存。

    在这里插入图片描述

    1. shmid 是 shmget 创建共享内存成功后获得的 id
    2. cmd 是如果想释放共享内存那么就用 IPC_RMID 选项
    3. buf 就有点类似于上面说的共享内存的属性 struct shm_ipc,这很少使用,先保留,设置为 NULL
  • 代码

    至此就完成了释放共享内存,这里可以验证一下。这里还加上了共享内存权限,可以看到运行结果,perms 就是权限。

    在这里插入图片描述

    请添加图片描述

💦 shmat

至此,我们完成了让进程在物理内存中创建好共享内存,然后释放共享内存,接下来还要将进程与共享内存关联。所以刚刚在查看共享内存时,nattch 就是与当前共享内存关联的进程的个数,可以看到这里只是创建了共享内存,还没有任何一个进程与之关联。

在这里插入图片描述

  • 认识接口

    在这里插入图片描述

    shmat 是系统提供于共享内存和进程关联的一个系统接口。

    1. shmid 是让这个进程和哪个共享内存关联。

    2. shmaddr 是要把共享内存挂接到进程的哪个虚拟上,这里要挂接到共享区,我们直接设置为 NULL,操作系统会帮我们选择。

    3. shmflg 的选项是挂接的方式,我们也不管,默认填 0。

    4. shmat 的返回值是 void*,与之类似的 malloc 的返回值也是 void*,虽然它们的区域不一样,但是原理类似,malloc 成功后返回值就是堆上的一块空间的起始地址,而 shmat 成功就返回关联共享内存段的起始地址。

  • 代码

    至此就完成了关联共享内存,这里可以验证一下。

    在这里插入图片描述

    请添加图片描述

💦 shmdt

当然有 shmgat 关联,也必须要有 shmdt 去关联。

  • 认识接口

    shmdt 是系统提供来取消关联共享内存的一个系统接口。shmdt 和 shmat 是同一个文档下,它就更简单了,只有一个参数。shmaddr 就是刚刚获取成功的共享内存的虚拟地址。

    在这里插入图片描述

  • 代码

    至此就完成了去关联共享内存,这里可以验证一下去关联共享内存。

    在这里插入图片描述
    请添加图片描述

💦 开始通信啦

至此创建共享内存、释放共享内存、关联共享内存、去关联共享内存这几个系统接口就介绍完了。那么 client 端一定比 server 端更简单,因为它不用在创建共享内存,自然也就不用它来释放共享内存,但是它需要关联和去关联共享内存。

  • 编写和完善代码,测试挂接数量由 0 - 1 - 2 - 1 - 0

    在这里插入图片描述

    请添加图片描述

  • 通信

    既然已经将物理内存映射到进程的地址空间,那么进程就可以直接使用虚拟地址直接对物理内存的真正访问,而不用再需要 read 和 write 这些系统调用接口了。
    此时 client 和 server 就看到了同一份资源。这里就可以通过指针访问共享内存了。然后 client 每隔 2 秒向共享内存写入 ABCD…XYZ,server 每 1 秒向共享内存读出 ABCD…XYZ。毫不意外的是这里 server 里是死循环,所以只要不终止,那么最后挂接数会由 2 变为 1,不过没关系,这里只是测试。可以看到如下测试结果,server 每一秒读一次,client 每二秒写一次,client 明显写的比较慢,但是 server 并没有等 client,所以共享内存机制并没有像管道机制那样有同步机制 (这里读的时候可以不休眠的读,就可以看到更明显的现象了,现象就不演示了,就是 client 2 秒写的时候,server 才不管你呢),所以共享内存不提供任何同步与互斥的操作,双方彼此独立,这里可能就会引起一些问题,比如 client 想写 Hello,然后让 server 干净的读,但是对于共享内存机制而言,server 只能等 client 写完才可以读。

    在这里插入图片描述

  • 完整代码

    makefile

    CC=gcc
    .PHONY:allclient:client.c$(CC) $^ -o $@
    server:server.c$(CC) $^ -o $@.PHONY:clean
    clean:rm -f client server
    

    common.h

    #pragma once#inlcude <stdio.h>#define PATH_NAME "/home/DanceBit/piping/shme"
    #define PROJ_ID 0x666
    #define SIZE 4097
    

    client.c

    #include "common.h"
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <sys/types.h>
    #include <unistd.h>int main()
    {//1.获取同一个keykey_t key = ftok(PATH_NAME, PROJ_ID);if(key < 0){perror("ftok");return 1;	}//2.获取共享内存int shmid = shmget(key, SIZE, IPC_CREAT);if(shmid < 0){perror("shmget");return 2;	}//3.挂接共享内存char* start = (char*)shmat(shmid, NULL, 0);//4.开始通信char c = 'A'while(c <= 'Z'){start[c - 'A'] = c;c++;sleep(2);	}//5.去挂接共享内存shmdt(start);return 0;	
    }
    

    server.c

    #include "common.h"
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <sys/types.h>
    #include <unistd.h>int main()
    {//1.创建keykey_t key = ftok(PATH_NAME, PROJ_ID);if(key < 0){perror("ftok");return 1;	}//2.创建共享内存int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0644);if(shmid < 0){perror("shmget");return 2;	}//3.挂接共享内存char* start = (char*)shmat(shmid, NULL, 0);//4.开始通信for( ; ; ){printf("%s\n", start);sleep(1);	}//5.去挂接共享内存shmdt(start);//6.释放共享内存shmctl(shmid, IPC_RMID, NULL);return 0;
    }
    

    请添加图片描述

  • 特性

    1. 共享内存的生命周期随 OS

    2. 共享内存不提供任何同步与互斥的操作,双方彼此独立

    3. 共享内存是进程间通信中速度最快的

      相比之下,管道就很慢了,它需要写端把数据写到管道,读端再从管道读,和管道的交互至少需要两次拷贝。还不包括如果写端的数据是从 stdin 中来的,那么就要先写到用户层缓冲区。

  • 说明

    1. 为什么创建共享内存的 SIZE 要设置成 4kb 的倍数 ❓

      因为系统在分配共享内存时,是按 4kb 也就是一页为单位,所以你申请 4097byte,那么操作系统在分配时会分配 4096 + 4096,也就是 8kb。但是 ipcs -m 时也确实只是 4097,这里系统确实是分配了 8kb,但是你能使用的就是你所申请的。换言之你申请了 4097byte,那么就有可能浪费 4095byte。所以在创建共享内存时,建议 SIZE 大小是 4kb 的整数倍。

    2. shmid 和 key

      key 是一个用户层生成的唯一键值,它的核心作用是为了区分唯一性,它不能用来进行 ipc 资源的操作;shmid 是一个系统给我们返回的 ipc 资源标识符 (其实它也是一个数组下标,它用于维护 ipc 资源),用来操作对应的 ipc 资源。这里 key 有点类似文件的 inode 号;shmid 有点类似文件的 fd。所以我们就能理解在代码或命令访问共享内存时使用的是 shmid,而不是 key,原因是无论是代码或命令都是用户层上的操作共享内存。

    3. 共享内存数据结构

      下图是操作系统给我们提供的一个系统调用头文件共享内存数据结构,而系统调用本来就是操作系统提供的。所以这个数据结构基本上和内核中描述共享内存的结构类似。struct shmid_ds 这个结构体就是我们在上面所说的 struct shm_ipc,系统中存在着大量的进程和对应的共享内存,所以每个共享内存创建出来都有这样一个结构。简单看下,其中有 shm_segsz 共享内存大小,shm_atime/shm_dtime 共享内存最近挂接和去挂接时间,shm_ctime 共享内存修改时间,shm_cpid 由 pid 进程创建,shm_lpid 由 pid 进程操作,shm_nattch 有几个进程挂接到共享内存,shm_unused 未使用的共享内存等。还有一个 shm_perm,我们找一下 ipc_perm。其中我们看到了熟悉的 key、mode。

      在这里插入图片描述

      在这里插入图片描述

      如下四、五后,我们发现文档中,消息队列中有 struct msqid_ds,其中也有 struct ipc_perm msg_perm;信号量中有 struct semid_ds,其中也有 struct ipc_perm msg_perm。这里只想说明,所有 system V 标准下的通信方案,都有一个描述其对应资源的结构体。

      在这里插入图片描述

      这里再简单提一下。可以看到这里的共享内存、消息队列、信号量结构体下第一行都有一个 struct ipc_perm xxx,那我们就可以定义一个数组 struct ipc_perm array[1024]; 我们都知道这里有一个嵌套结构体,假设只知道内部 obj 的地址,那么 struct A a 的地址就同 &a.obj,此时 (struct A*)&a.obj,那么就可以访问 x 和 y 了。所以 Linux 就将所有的 ipc_perm 放在一个数组中,然后 &ipc_perm 再强制类型转换成共享内存或消息队列或信号量类型。换言之 Linux 内核的 ipc 资源可以用数组来维护。也就是说将来想创建一个 ipc 资源,然后系统给你一个 ipc_perm,然后再给你对应的 ipc 资源的其它属性,使用 key 值保证唯一性,然后再把数组的下标返回。

      在这里插入图片描述

      这里只能是粗略的谈一下,这块要真正搞懂就必须得搞清楚它对应的数据结构,这个数据结构非常非常的复杂。可以看到如下 Linux 内核框架图,sem_array,msg_queue,shmid_kerne 它们的第一个成员都是 xxx.perm,经过强转就可以访问 kern_i
      pc_perm。

      在这里插入图片描述

四、system V ---- 消息队列 (了解)

操作系统会在系统中维护一个消息队列,这个消息队列默认情况下是空的,当用户 1 创建消息队列时,就会用 key 来标识其唯一性,此时用户 2 就可以通过 key 来获取这个消息队列,这时两用户就可以看到同一个消息队列了。然后用户 1 就可以往这个消息队列里放节点,用户 2 自然也能看到,反之也可以。这就是消息队列。

在这里插入图片描述

  • 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法

  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同类型值

  • 特性:IPC 资源必须删除,否则不会自动清除,除非重启,所以 system V IPC 资源生命周期随内核

  • 接口

    msgget 获取消息队列,msgctl 释放消息队列,msgsnd 发送消息队列,msgrcv 接收消息队列。

    在这里插入图片描述

五、system V ---- 信号量 (了解)

信号量虽然也只是了解,不过这里根据铺垫一些概念,它与上面的一些内容有些关联,且为后面的多线程铺垫。

进程通信的本质是让进程看到同一份资源,当同一份资源被多进程看到时,极有可能出现 A 进程正在对空间进行写入,B 进程就来读取了,如果像管道那样自带同步机制倒也不会影响,实际上上面所谈的共享内存就是一种读写错乱的机制。这里我们把当多个进程看到的那一份资源叫做临界资源。到此我们还观察到在 server 和 client 中访问共享内存/临界资源的代码实际只有少部分几行,换言之,可能造成读写数据不一致问题的就是这一部分代码,我们把这部分访问临界资源的代码叫做临界区。所以为了必免数据不一致,保护临界资源,需要对临界区代码进行某种保护,而这某种保护这里就谈互斥(当然后面还有同步),所谓互斥就是有一块空间,在任何时候有且只能有一个进程在进行访问 (生活中最典型的互斥场景就是上厕所),互斥本身是一种串行化执行 (也就是说共享内存中就是因为并行读写执行才导致的数据不一致问题),而后面一般互斥是通过来完成,这里可以提一种二元信号量来完成串行执行 (这里我们也能猜到加锁和解锁是有代码的) 。所以串行化的过程本质是对临界区资源加锁和解锁,完成互斥操作。也就是说 client 和 server 它们都必须遵守你要进入临界区你得加锁,退出临界区,你得解锁。

这里再感性的理解一遍原子性概念,白话就是要么做了,要么没做。比如一个进程想往共享内存里写 Hello World,写完 Hello 时这个状态叫做写入中,在写入过程中,不能被打搅,直到全部写完。也就是说在外人看来这里写入过程的状态只有两种,其一是还没写,其二是写完了。这就是原子性,最典型的应用就是你在招商银行中有 1000 元,农业银行中有 500 元,然后你想进行转帐:招商账号 -= 200;农业账号 += 200;其中当你刚从招商账号转账到农业账号时,系统崩溃了,此时农业银行还是 500,但是招商银行少了 200。这种现象就是某个任务正在进行时,突然因为某些原因而导致任务中断,这就叫做不是原子性。所以这个转账的过程要么就不做,要么就做成功,或者转账失败了也能保证招商银行的钱不受影响,这就是原子性。其中后面我们也可以采用互斥的方案来保证原子性。

  • 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程竞争使用这些资源,进程的这种关系为进程的互斥
  • 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源
  • 在进程中涉及到互斥资源的程序段叫做临界区
  • 特性:IPC资源必须删除,否则不会自己清除,除非重启,所以 system V IPC 资源的生命周期随内核

什么是信号量 | 为什么要有信号量 | 怎么用信号量 ❓

信号量也叫做信号灯。生活中,你买了一个房子,你虽然没住在里面,但房子依旧是你的;你在宿舍,你虽然没躺着,但是那个床位依旧是你的;你在网上买票看电影,你虽然还没看,但你清楚到了一定的时间你就可以看到;所以现实生活中有很多 “ 预定机制 ”,但是不提前享受,在卖票时,要保证一人一座,不能超过电影院的承受能力。

这里有三个进程都要访问共享内存,这块共享内存就是这三个进程的临界资源,而要访问共享内存需要加锁,这里进程 A 先访问成功,然后解锁,紧接着进程 B,然后又进程 C,这是互斥。

在这里插入图片描述

而有时进程不是把共享内存所有使用,这里共享内存被分为三块空间,有诸多进程,如果想让不同的进程访问不同的共享内存区域,那么它们是不影响的,但是最怕的是一个进程正在访问一块空间,而另一个进程也来访问这块空间。这里想说明的是进程不是对共享内存整体访问,而是可能只使用共享内存中的一部分,所以只要多个进程访问的那部分共享内存是不重叠的,那么就可以并行访问。也就是说有七八个进程,每个进程都把这个共享内存占有就是互斥,这显然不太合理;所以允许在访问共享内存不重叠时前提,可以允许少量进程同时访问,而这样的工作就是由信号量来完成的。

在这里插入图片描述

信号量本质是一个计数器 int count (注意这里的 int count 是错误的,先暂时理解,后面会解释),然后定义 int count = 3; 还有一段伪代码,任何进程想操作共享内存前必须先申请信号量。然后进程 A 要进来,所以 count- - 后,count 是 2,而进程 A 要出去,也要对应 count++。这就类似于电影院的预订机制,电影院有 100 张票,你预定了一张票,票数就变成 99 张,而当你看完离开后,票数就变成 100 张。也就是说当 count- - 后,一定有资源给你预留,而不是 pause,这里一共有 3 个资源,你已经申请一个了,即使你还没有开始访问,你最终也能访问,这就是一种预订,这就是信号量。所以信号量本质是计数器没错,是用来描述临界资源中,资源数目的计数器。
每个进程想对共享内存访问都必须先申请信号量,我们称之为p操作,而访问完,要执行非临界区代码时,要释放信号量,我们称之为v操作,所以信号量最重要的操作我们称之为pv原语

在这里插入图片描述

同时有 5 个进程都想访问共享内存,都想对计数器减减,这里有两个问题。其一,多个进程能不能操作同一个 count 值 ? —— 不能,因为有写时拷贝,你定义全局变量,甚至 malloc,不管如何,只要子进程去操作时,不可能减减加加去影响其它进程的,count 一开始是 3,每个进程写时拷贝都认为是 3。所以信号量 != count,因为必须保证多个进程操作的是同一个信号量;其二,信号量是干什么的 ?—— 保护临界资源的安全性。
假设还认为信号量是一个类似全局变量,且多个进程能操作一个全局变量 count,那么每个进程去执行上面的伪代码不就行了吗 ?—— 不行,因为申请信号量过程中:0) if 判断 1) 内存 --> cpu 2) cpu 执行计算 3) cpu --> 内存。而此时进程 A 执行判断成功后,进程 B 已经减到 0 了,进程 A 再减就是 -1,相当于给别人多分配了资源,因为它是多条语句构成,有可能会导致操作乱序,有可能会多分配资源出去,所以就不是原子性的。
每个进程都得先申请信号量,前提是每个进程都得先看到信号量。但如果每个进程都能看到信号量时,信号量本身就是一个临界资源,所以这样就变成了信号量原本是保护临界资源的,但是自己却变成了临界资源。这里当然有问题,你要保护其它人,但前提是先保护自己的安全。所以上面所谈信号量 pv 操作,它本身就是原子的,所以它被称为 pv 原语,人话就是那个计数器本身就是原子的,同一时间内,它只允许一个进程进行操作。
实现伪代码 —— 假设这里有若干个进程要访问临界资源,那么首先只有进程 A 先申请锁成功,然后往下执行后 count = 2 解锁,进程 A 就可以访问共享内存的一部分了。另外进程 B 也在申请锁成功,然后往下执行后 count = 1 解锁,进程 B 就可以访问共享内存的一部分了。再另外进程 C … … count = 0 解锁,进程 C 就可以访问共享内存的一部分了。再另外进程 D 也申请锁成功,但是因为 count = 0,代表无多余的资源,此时就 goto 跳转到 begin,重复执行,此时就用这段代码,约束了访问临界资源的进程。再然后进程 A 访问完毕,然后申请锁成功,count++ 变成 1,最后解锁成功。此时进程 D 申请锁成功,count 是 1 表示有资源可以访问,然后往下执行 count = 0 解锁,进程 D 就可以访问共享内存的一部分了。

在这里插入图片描述

在多进程环境下,如何保证信号量被多个进程看到 ?—— semget,semctl,ftok,所以我发们只要使用系统提供的一批接口,就可以保证信号量被多个进程看到。
如果信号量计数器的值是 1,此时信号量的值无非就是 1 或 0,你要申请信号量,但只让你一个进程申请成功,这种信号量叫做二元信号量,其本质就是一种互斥语义。换言之,信号量计数器的值 大于 1,它就是多元信号量。


在这里插入图片描述

简单提一下:

  • semget 中 nsems 是系统可以允许你一次创建多个信号量,底层是用数组来维护这多个信号量,所以 ipcs -s 时 ,你会发现它是一个信号量数组。

    在这里插入图片描述

  • semctl 中 semnum 是你想对第几个信号量进行操作。

  • semop 是需要对特定的信号量传入 sembuf 结构,这个结构如下图,sem_op 对应上面所说的 pv 操作,如果是 -1,就表示对计数器 -1,如果是 +1,就表示对计数器 +1。nsops 是想对第几个信号量操作。

    在这里插入图片描述

写在后面

  • 共享内存的优点是所以进程间通信速度最快的,缺点是不会维护同步和互斥机制
  • 这里还有一个 mmap 我们不打算涉及,其原理类似共享内存,有兴趣自行了解
  • 可以看到 system V 标准下的 ipc 共享内存机制其实挺复杂的, 但其实共享内存又是 system V 标准下最简单的一套机制,所以当你看到这里的时候其实也不难,相对更复杂的是消息队列机制,最复杂的是信号量机制。实际在公司中很少自己写这些东西,特别是消息队列和信号量,所以目前就先了解共享内存机制,知道是其底层是怎么通信的就好。

这篇关于【Linux系统编程:进程间通信】匿名管道 pipe | 命名管道 fifo | system V --- 共享内存 shmget shmctl shmat shmdt,消息队列,信号量的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/236247

相关文章

Linux磁盘分区、格式化和挂载方式

《Linux磁盘分区、格式化和挂载方式》本文详细介绍了Linux系统中磁盘分区、格式化和挂载的基本操作步骤和命令,包括MBR和GPT分区表的区别、fdisk和gdisk命令的使用、常见的文件系统格式以... 目录一、磁盘分区表分类二、fdisk命令创建分区1、交互式的命令2、分区主分区3、创建扩展分区,然后

Linux中chmod权限设置方式

《Linux中chmod权限设置方式》本文介绍了Linux系统中文件和目录权限的设置方法,包括chmod、chown和chgrp命令的使用,以及权限模式和符号模式的详细说明,通过这些命令,用户可以灵活... 目录设置基本权限命令:chmod1、权限介绍2、chmod命令常见用法和示例3、文件权限详解4、ch

Linux内核之内核裁剪详解

《Linux内核之内核裁剪详解》Linux内核裁剪是通过移除不必要的功能和模块,调整配置参数来优化内核,以满足特定需求,裁剪的方法包括使用配置选项、模块化设计和优化配置参数,图形裁剪工具如makeme... 目录简介一、 裁剪的原因二、裁剪的方法三、图形裁剪工具四、操作说明五、make menuconfig

Linux使用nohup命令在后台运行脚本

《Linux使用nohup命令在后台运行脚本》在Linux或类Unix系统中,后台运行脚本是一项非常实用的技能,尤其适用于需要长时间运行的任务或服务,本文我们来看看如何使用nohup命令在后台... 目录nohup 命令简介基本用法输出重定向& 符号的作用后台进程的特点注意事项实际应用场景长时间运行的任务服

什么是cron? Linux系统下Cron定时任务使用指南

《什么是cron?Linux系统下Cron定时任务使用指南》在日常的Linux系统管理和维护中,定时执行任务是非常常见的需求,你可能需要每天执行备份任务、清理系统日志或运行特定的脚本,而不想每天... 在管理 linux 服务器的过程中,总有一些任务需要我们定期或重复执行。就比如备份任务,通常会选在服务器资

TP-LINK/水星和hasivo交换机怎么选? 三款网管交换机系统功能对比

《TP-LINK/水星和hasivo交换机怎么选?三款网管交换机系统功能对比》今天选了三款都是”8+1″的2.5G网管交换机,分别是TP-LINK水星和hasivo交换机,该怎么选呢?这些交换机功... TP-LINK、水星和hasivo这三台交换机都是”8+1″的2.5G网管交换机,我手里的China编程has

Linux限制ip访问的解决方案

《Linux限制ip访问的解决方案》为了修复安全扫描中发现的漏洞,我们需要对某些服务设置访问限制,具体来说,就是要确保只有指定的内部IP地址能够访问这些服务,所以本文给大家介绍了Linux限制ip访问... 目录背景:解决方案:使用Firewalld防火墙规则验证方法深度了解防火墙逻辑应用场景与扩展背景:

SpringBoot 自定义消息转换器使用详解

《SpringBoot自定义消息转换器使用详解》本文详细介绍了SpringBoot消息转换器的知识,并通过案例操作演示了如何进行自定义消息转换器的定制开发和使用,感兴趣的朋友一起看看吧... 目录一、前言二、SpringBoot 内容协商介绍2.1 什么是内容协商2.2 内容协商机制深入理解2.2.1 内容

Linux下MySQL8.0.26安装教程

《Linux下MySQL8.0.26安装教程》文章详细介绍了如何在Linux系统上安装和配置MySQL,包括下载、解压、安装依赖、启动服务、获取默认密码、设置密码、支持远程登录以及创建表,感兴趣的朋友... 目录1.找到官网下载位置1.访问mysql存档2.下载社区版3.百度网盘中2.linux安装配置1.

C#如何优雅地取消进程的执行之Cancellation详解

《C#如何优雅地取消进程的执行之Cancellation详解》本文介绍了.NET框架中的取消协作模型,包括CancellationToken的使用、取消请求的发送和接收、以及如何处理取消事件... 目录概述与取消线程相关的类型代码举例操作取消vs对象取消监听并响应取消请求轮询监听通过回调注册进行监听使用Wa