Linux系统编程:通过System V共享内存实现进程间通信

目录

一. 共享内存实现进程间通信的原理

二. 共享内存相关函数

2.1 共享内存的获取 shmget / ftok

2.2 共享内存与进程地址空间相关联 shmat

2.3 取消共享内存与进程地址空间的关联 shmdt

2.4 删除共享内存 shmctl

2.5 通信双方创建共享内存代码

三. 共享内存实现进程间通信

3.1 实现方法及特性

3.2 为共享内存添加访问控制

四. 总结


一. 共享内存实现进程间通信的原理

要实现进程间通信,就必须让相互之间进行通信的进程看到同一份资源(同一块内存空间),如通过管道实现进程间通信,本质就是让两个进程分别以读和写的方式打开同一份管道文件,一个进程向管道中写数据,另一个进程再从管道中将数据读出,这样两个进程就可以看到同一份内存空间,从而实现了进程间通信。

System V共享内存实现进程间通信的方式与管道相同,区别在于管道是基于文件的,而共享内存则是直接申请内存空间,不需用进行文件相关操作通过System V共享内存实现通信的进程,都会使用物理内存中的同一块空间,这一块公共的物理内存空间经过通信双方进程的页表,映射到进程地址空间的共享区,通信双方进程在运行期间,拿到共享区虚拟地址,通过页表映射,就可以看到同一块物理内存,就可以实现进程间通信。

如果操作系统内有多组通过System V共享内存方式相互通信的进程处于运行状态,那么就会存在多组共享内存,操作系统需要对这些共享内存空间进行管理,管理方式为:先通过struct结构体进行描述,再利用特定的数据结构组织

可以这样理解:共享内存 = 共享的物理内存 + 对应的内核级数据结构。

图1.1 共享内存的实现原理

二. 共享内存相关函数

共享内存实现进程间通信的步骤可以总结为:创建共享内存 -> 共享内存与地址空间相关联 -> 通信 -> 共享内存与地址空间解绑 -> 销毁共享内存。

2.1 共享内存的获取 shmget / ftok

shmget函数:获取共享内存

头文件:#include<sys/ipc.h>、#include<sys/shm.h>

函数原型:int shmget(key_t key, size_t size, int shmflg)

函数参数:

        key -- 特定共享内存的标识符

        size -- 共享内存的大小

        shmflg -- 共享内存获取的权限参数

返回值:创建成功返回共享内存的编号(称为shmid),失败返回-1

共享内存标识符key:OS中可能存在多个共享内存,需要保证通信双方看到同一块共享内存,因此,每个共享内存都需要一个特定的key值进行区分,这个key值是多少并不重要,只要保证它在OS中是唯一的即可通信双方进程(Serve && Client)需要约定相同的算法,保证他们可以使用shmget获取到同一块共享内存

ftok函数可以用于获取key值,只要调用ftok的实参相同,就会返回相同的key值。

ftok函数:获取共享内存标识符key

头文件: #include<sys/ipc.h>、#include<sys/types.h>

函数原型:key_t ftok(const char* pathname, int proj_id);

函数参数:

        pathname:项目(文件)路径

        proj_id:项目(文件)的id编号

返回值:成功返回特定的key值,否则返回-1。

共享内存大小size:以字节为单位,建议取页(PAGE:4096bytes)大小的整数倍,因为如果获取共享内存空间的大小不是页大小的整数倍,OS就会向上取整申请到页大小整数倍的内存空间,但是多申请的空间却不能被用户所使用。如,申请4097bytes的共享内存,OS会实际申请2*4096bytes的空间,而能被使用的只有4097bytes,剩下的都浪费掉了。

权限参数shmflg:有IPC_CREAT、IPC_EXCL、共享内存起始权限码、0这几种选项,他们之间通过竖划线 | 隔开,每个选项都有其意义。

  • IPC_CREAT:如果key标识的共享内存存在,就直接将其获取,如果不存在,就创建。
  • IPC_EXCL:单独使用没有任何意义,一般配合IPC_CREAT使用,IPC_CREAT | IPC_EXCL表示如果共享内存不存在就将其创建,如果存在直接报错,这样可以保证获取到的共享内存是一块全新的共享内存。
  • 起始权限码:用户对于这块共享内存的使用权限,如0666就表示拥有者、所属组、其他人就具有读写权限。
  • 0:只能获取已经存在的共享内存,不能创建新的,不存在就报错。

一般而言,通信双方分别以 IPC_CREAT | IPC_EXCL 和 0 的方式获取共享内存,确保一方创建全新的共享内存,另一方只能获取到该共享内存(传0阻断不存在创建新共享内存的可能)。

代码2.1以 IPC_CREAT | IPC_EXCL | 0666 的方式获取共享内存,运行代码,就可以成功获取共享内存,但是当第二次运行代码,却发现运行出错了(见图2.1),这是因为该共享内存再第一次程序运行后被创建,存在于操作系统中,IPC_CREAT | IPC_EXCL获取的共享内存一定是全新的,因此第二次运行程序会失败,删除该共享内存之后才可以再次成功运行。

结论:共享内存的生命周期是随OS内核的,而不是随进程的。

代码2.1:获取共享内存

// common.hpp -- 头文件
#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>#define PATH_NAME "."
#define PROJ_ID 0x66
#define SIZE 4096// shmServe.cc -- 客户端代码源文件(用于接收信息)
#include "common.hpp"int main()
{// 获取共享内存key值key_t k = ftok(PATH_NAME, PROJ_ID);   if(k == -1){perror("ftok");exit(1);}// 创建共享内存int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);if(shmid == -1){perror("shmget");exit(2);}printf("Serve# 共享内存获取成功,shmid:%d\n", shmid);return 0;
}
图2.1 代码2.1的两次运行结果

这里介绍两条指令,分别用于查看共享内存信息和删除共享内存:

  • ipcs -m 指令:查看系统中所有共享内存的详细信息。
  • ipcrm -m [shmid]:通过指定共享内存的shmid来删除指定的共享内存。

当然,也可以通过代码删除共享内存,本文后面会讲解。

图2.2 通过指令查看共享内存的属性信息和删除共享内存

2.2 共享内存与进程地址空间相关联 shmat

shmat函数:将共享内存关联到进程地址空间

头文件:#include<sys/types.h>、#include<sys/shm.h>

函数原型:void* shmat(int shmid, const void* shmaddr, int shmflg)

函数参数:

        shmid:进行挂接的共享内存的shmid

        shmaddr:指定挂接的虚拟地址(传NULL表示让OS自动选择挂接地址)

        shmflg:挂接权限相关参数

返回值:若成功返回挂接到的虚拟地址,失败返回nullptr

挂接地址shmaddr参数:由于我们并不可知虚拟地址的具体使用情况,所以这个参数基本都是传NULL/nullptr来让OS自动选择虚拟地址进行关联。 

挂接权限shmflg:如果传SHM_RDONLY,这表示对应共享内存空间只有读权限,传其他都是读写权限,一般shmflg都传实参0。

当共享内存与虚拟地址关联期间,使用ipcs -m指令查看共享内存属性信息,nattch就会变为1,如果通信双方都与共享内存进行了关联,那么nattch就是2。

2.3 取消共享内存与进程地址空间的关联 shmdt

shmdt函数:让共享内存与当前进程脱离

头文件:#include<sys/types.h>、#include<sys/shm.h>

函数原型:int shmdt(const char* shmaddr)

返回值:成功返回0,失败返回-1

2.4 删除共享内存 shmctl

通过共享内存控制shmctl函数(共享内存控制函数),可以删除共享内存。

删除共享内存的操作只要通信双方有一方指向即可,否则会造成重复删除。一般而言,读取信息的进程创建新的共享内存,也负责删除共享内存,遵循谁创建、谁删除的原则。

shmctl函数:控制共享内存

头文件:#include<sys/ipc.h>  #include<sys/shm.h>

函数原型:int shmctl(int shmid, int cmd, struct shmid_ds* buf)

函数参数:

        shmid -- 共享内存的shmid

        cmd -- 控制指令,选择操作

        buf -- 指向描述共享内存属性信息的结构体指针

返回值:成功返回非负数,失败返回-1

形参cmd可以选择具体的控制策略:

  • IPC_STAT -- 以buf为输出型参数,获取共享内存的属性信息。
  • IPC_SET -- 设置共享内存的属性为buf指向的内容。
  • IPC_RMID -- 删除共享内存,此时buf传空指针NULL。

2.5 通信双方创建共享内存代码

代码2.2:头文件common.hpp -- 由通信双方共同包含

#pragma once#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>#define PATH_NAME "."
#define PROJ_ID 0x66
#define SIZE 4096

代码2.3:服务端代码shmServe.cc -- 用于数据读取

#include "common.hpp"int main()
{// 获取共享内存key值key_t k = ftok(PATH_NAME, PROJ_ID);   if(k == -1){perror("Serve ftok");exit(1);}printf("Serve# 成功获取key值,key:%d\n", k);// 创建共享内存int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);if(shmid == -1){perror("Sreve shmget");exit(2);}printf("Serve# 共享内存获取成功,shmid:%d\n", shmid);// 将共享内存与进程相关联char* shmaddr = (char*)shmat(shmid, NULL, 0);if(shmaddr == nullptr){perror("Serve shmat");exit(3);}printf("Serve# 共享内存与进程成功关联,shmid:%d\n", shmid);// 通信代码// ... ...// 让共享内存脱离当前进程int n = shmdt(shmaddr);if(n == -1){perror("Serve shmdt");exit(4);}printf("Serve# 共享内存成功脱离进程,shmid:%d\n", shmid);// 删除共享内存n = shmctl(shmid, IPC_RMID, NULL);if(n == -1){perror("Serve shmctl");exit(5);}printf("Serve# 共享内存删除成功,shmid:%d\n", shmid);return 0;
}

代码2.4:客户端代码shmClient.cc -- 用于数据发送

#include "common.hpp"int main()
{// 获取共享内存key值key_t k = ftok(PATH_NAME, PROJ_ID);   if(k == -1){perror("Client ftok");exit(1);}printf("Client# 成功获取key值,key:%d\n", k);// 创建共享内存int shmid = shmget(k, SIZE, 0);if(shmid == -1){perror("Client shmget");exit(2);}printf("Client# 共享内存获取成功,shmid:%d\n", shmid);// 将共享内存与进程相关联char* shmaddr = (char*)shmat(shmid, NULL, 0);if(shmaddr == nullptr){perror("Client shmat");exit(3);}printf("Client# 共享内存与进程成功关联,shmid:%d\n", shmid);// 通信代码// ... ...// 让共享内存脱离当前进程int n = shmdt(shmaddr);if(n == -1){perror("Client shmdt");exit(4);}printf("Client# 共享内存成功脱离进程,shmid:%d\n", shmid);return 0;
}

三. 共享内存实现进程间通信

3.1 实现方法及特性

在数据输入端(shmClient),我们可以将共享内存视为一块通过malloc得来的char*指向的一段动态内存空,可以使用printf系列函数向这块空间写数据,或者将共享内存空间视为数组,使用下标的形式给每个位置赋值,这样就实现了将数据写入共享内存。

在数据读取端(shmServe),可以将共享内存视为一个大字符串,通过特定的方式,从这个大字符串中获取数据即可。

代码3.1和代码3.2实现了共享内存进程间通信的简单逻辑,在shmClient端,通过下标访问的方式,每隔3s写一次数据,在shmServe端,每隔1s读取一次数据。先运行shmServe端代码,间隔几秒后运行shmClient端代码,根据图3.1展示的运行结果,shmServe端在shmClient端开始运行之前就开始读取共享内存中的内容,在shmClient运行起来后,由于读快写慢,shmClient写入的内容在shmServe端被多次读取,可见,共享内存,没有访问控制。

结论1:共享内存没有访问控制。

代码3.1:shmClient端发送数据

    // 通信代码char ch = 'a';int count = 0;for(; ch <= 'c'; ++ch){shmaddr[count++] = ch;printf("write succsee# %s\n", shmaddr);sleep(3);}snprintf(shmaddr, SIZE, "quit");

代码3.2:shmServe端读取数据 

    // 通信代码while(true){printf("[Client say]# %s\n", shmaddr);if(strcmp(shmaddr, "quit") == 0) break;sleep(1);}
图3.1 共享内存通信读写双方代码执行结果

通过观察上面的代码我们发现,用户可以直接向共享内存中写数据和从共享内存中读取数据,不需要经过用户级缓冲区,共享内存的读或写操作最少只需要一次拷贝即可完成。而通过管道进行读写,则需要将数据预先写入或读入缓冲区,才可以写入管道文件或读出。图3.2为使用管道和共享内存的方法进行进程间通信时,读和写操作涉及的数据拷贝情况,管道通信至少要进行两次数据拷贝,而共享内存可以只进行一次数据拷贝,因此共享内存是一种高效的进程间通信手段。

结论2:共享内存进行进程间通信,通信的一方向共享内存中写入数据,通信的另一方马上就能读取到数据,不需要向操作系统中拷贝数据,共享内存是所有进程间通信方法中效率最高的。

图3.2 管道和共享内存实现进程间通信的资源拷贝情况

管道通信的特性总结:

  1. 不具有访问控制,存在并发问题。
  2. 不需要向OS内核中拷贝数据,通信效率高。

3.2 为共享内存添加访问控制

通过使用命名管道加以辅助,就可以为共享内存添加访问控制,具体的实现方法和原理为:

  • 在读端(shmServe)程序开始运行时创建命名管道文件,程序运行结束后管道文件销毁。
  • 在写端(shmClient)向共享内存中写入数据后,向管道文件中写入任意的、少量的数据,在读端(shmServe)获取共享内存内容之前,先读取管道中的资源,如果写端没有将期望的数据全部写入共享内存,那么就不会向管道中写数据,读端就必须阻塞等待管道中被写入数据,也就无法获取共享内存中的数据。只有当写端完成向共享内存中写入一次数据,然后向管道文件中写入数据让读端读到了管道资源后,读端代码才可以继续运行,获取到共享内存中的资源。

代码3.3 ~ 3.5,为通过管道为共享内存添加访问控制的实现代码。

代码3.3:common.hpp头文件 -- 被通信双方源文件包含

#pragma once#include <iostream>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>#define PATH_NAME "."
#define PROJ_ID 0x66
#define SIZE 4096#define FIFO_NAME "fifo.ipc"
#define MODE 0666// 定义类,其构造和析构函数可以创建和销毁管道文件
class Init
{
public:Init(){int n = mkfifo(FIFO_NAME, MODE);if(n == -1) perror("mkfifo");assert(n != -1);(void)n;}~Init(){int n = unlink(FIFO_NAME);assert(n != -1);(void)n;}
};#define READ O_RDONLY 
#define WRITE O_WRONLY// 管道文件打开函数
int OpenFifo(const char* pathname, int flags)
{int fd = open(pathname, flags);assert(fd != -1);return fd;
}// 等待函数 -- 用于读端访问控制
// 管道内没有资源时就阻塞
void Wait(int fd)
{uint32_t temp = 0;ssize_t sz = read(fd, &temp, sizeof(uint32_t));assert(sz == sizeof(uint32_t));(void)sz;
}// 唤醒函数 -- 用于写端进程控制
// 向管道内写数据,终止读端进程的阻塞等待
void WakeUp(int fd)
{uint32_t temp = 1;ssize_t sz = write(fd, &temp, sizeof(uint32_t));assert(sz == sizeof(uint32_t));(void)sz;
}// 管道关闭函数
void CloseFifo(int fd)
{close(fd);
}

代码3.4:读端源文件(shmServe.cc)代码

#include "common.hpp"// 全局类对象
// 构造和析构函数分别负责管道文件的创建和销毁
Init init;int main()
{// 获取共享内存key值key_t k = ftok(PATH_NAME, PROJ_ID);   if(k == -1){perror("Serve ftok");exit(1);}printf("Serve# 成功获取key值,key:%d\n", k);// 创建共享内存int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);if(shmid == -1){perror("Sreve shmget");exit(2);}printf("Serve# 共享内存获取成功,shmid:%d\n", shmid);// 将共享内存与进程相关联char* shmaddr = (char*)shmat(shmid, NULL, 0);if(shmaddr == nullptr){perror("Serve shmat");exit(3);}printf("Serve# 共享内存与进程成功关联,shmid:%d\n", shmid);// 通信代码int fd = OpenFifo(FIFO_NAME, READ);   // 只读方式打开管道文件while(true){Wait(fd);   // 等待读取printf("[Client say]# %s\n", shmaddr);if(strcmp(shmaddr, "quit") == 0) break;}// while(true)// {//     printf("[Client say]# %s\n", shmaddr);//     if(strcmp(shmaddr, "quit") == 0) break;//     sleep(1);// }// 让共享内存脱离当前进程int n = shmdt(shmaddr);if(n == -1){perror("Serve shmdt");exit(4);}printf("Serve# 共享内存成功脱离进程,shmid:%d\n", shmid);// 删除共享内存n = shmctl(shmid, IPC_RMID, NULL);if(n == -1){perror("Serve shmctl");exit(5);}printf("Serve# 共享内存删除成功,shmid:%d\n", shmid);CloseFifo(fd);return 0;
}

代码3.5:写端源文件(shmClient.cc)代码

#include "common.hpp"int main()
{// 获取共享内存key值key_t k = ftok(PATH_NAME, PROJ_ID);   if(k == -1){perror("Client ftok");exit(1);}printf("Client# 成功获取key值,key:%d\n", k);// 创建共享内存int shmid = shmget(k, SIZE, 0);if(shmid == -1){perror("Client shmget");exit(2);}printf("Client# 共享内存获取成功,shmid:%d\n", shmid);// 将共享内存与进程相关联char* shmaddr = (char*)shmat(shmid, NULL, 0);if(shmaddr == nullptr){perror("Client shmat");exit(3);}printf("Client# 共享内存与进程成功关联,shmid:%d\n", shmid);// 通信代码int fd = OpenFifo(FIFO_NAME, WRITE);while(true){ssize_t sz = read(0, shmaddr, SIZE);    // 共享内存从键盘中读入数据(换行符也被写入)assert(sz >= 0);shmaddr[sz - 1] = '\0';   //末尾添加'\0'表示终止WakeUp(fd);    // 唤醒读端进程if(strcmp(shmaddr, "quit") == 0) break;}// char ch = 'a';// int count = 0;// for(; ch <= 'c'; ++ch)// {//     shmaddr[count++] = ch;//     printf("write succsee# %s\n", shmaddr);//     sleep(3);// }// snprintf(shmaddr, SIZE, "quit");// 让共享内存脱离当前进程int n = shmdt(shmaddr);if(n == -1){perror("Client shmdt");exit(4);}printf("Client# 共享内存成功脱离进程,shmid:%d\n", shmid);CloseFifo(fd);return 0;
}

四. 总结

  • System V共享内存实现进程间通信的底层原理是通信双方进程看到同一块内存,位于物理内存上的共享内存块,通过页表映射到通信双方的进程地址空间的共享区,通信双方拿到共享区的虚拟地址,通过页表映射,访问到同一块物理内存。
  • 使用System V共享内存实现进程间通信的操作流程为:通过ftok函数获取唯一的共享内存标识符key -> 通过shmget函数获取共享内存 -> 通过shmat函数让共享内存和进程绑定 -> 【进行进程通信】-> 通过shmdt函数让共享内存和进程脱离 -> 通过shmctl删除共享内存。
  • System V共享内存 进程间通信的特点为:(1)不需要向操作系统内核中拷贝数据,是所有进程间通信的方法中效率最高的。(2)没有访问控制。
  • 通过管道的辅助,可以为 System V共享内存 进程间通信添加访问控制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/70613.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NLP——操作步骤讲义与实践链接

数据集与语料 语料是NLP的生命之源&#xff0c;所有NLP问题都是从语料中学到数据分布的规律语料的分类&#xff1a;单语料&#xff0c;平行语料&#xff0c;复杂结构 语料的例子&#xff1a;Penn Treebank, Daily Dialog, WMT-1x翻译数据集&#xff0c;中文闲聊数据集&#xf…

网络通信TCP/IP协议逐层分析数据链路层(第四十课)

Ethernet Ⅱ帧,也称为Ethernet V2帧,是如今局域网里最常见的以太帧,是以太网事实标准。如今大多数的TCP/IP应用(如HTTP、FTP、SMTP、POP3等)都是采用Ethernet II帧承载。 1、MAC地址概述 -MAC地址,即以太网地址,用来标识一个以太网上的某个单独设备或一组设备 -长度…

用递归与迭代完成二叉树的三种遍历

目录 二叉树的前序遍历 题目 前序遍历题目链接 递归代码 1.利用方法返回值的代码 2.返回值为void的代码 非递归实现前序遍历(利用栈stack) 1.利用方法返回值的代码 2.返回值为void的代码 二叉树的中序遍历 题目 :给定一个二叉树的根节点 root &#xff0c;返回 它…

windows pip安装出现 error: Microsoft Visual C++ 14.0 is required

可参考&#xff1a;如何解决 Microsoft Visual C 14.0 or greater is required. Get it with “Microsoft C Build Tools“_不吃香菜的小趴菜的博客-CSDN博客 一、安装Visual Studio2022 1、下载&#xff1a;下载 Visual Studio Tools - 免费安装 Windows、Mac、Linux 我这使…

Linux 终端命令之文件浏览(1) cat

Linux 文件浏览命令 cat, more, less, head, tail&#xff0c;此五个文件浏览类的命令皆为外部命令。 hannHannYang:~$ which cat /usr/bin/cat hannHannYang:~$ which more /usr/bin/more hannHannYang:~$ which less /usr/bin/less hannHannYang:~$ which head /usr/bin/he…

24近3年内蒙古大学自动化考研院校分析

今天给大家带来的是内蒙古大学控制考研分析 满满干货&#xff5e;还不快快点赞收藏 一、内蒙古大学 学校简介 内蒙古大学位于内蒙古自治区首府、历史文化名城呼和浩特市&#xff0c;距北京400余公里&#xff0c;是中华人民共和国成立后党和国家在民族地区创办的第一所综合大…

【【verilog典型电路设计之FIR滤波器的设计】】

verilog典型电路设计之FIR滤波器的设计 我们常用的FIR滤波器称为有限冲激响应 是一种常用的数字滤波器 &#xff0c;采用对已输入样值的加权和来形成它的输出。 对于输入序列X[n] 的FIR滤波器可用下图所示的结构示意图来表示&#xff0c;其中X[n] 是输入数据流。各级的输入连…

java八股文面试[java基础]——String StringBuilder StringBuffer

String类型定义&#xff1a; final String 不可以继承 final char [] 不可以修改 String不可变的好处&#xff1a; hash值只需要算一次&#xff0c;当String作为map的key时&#xff0c; 不需要考虑hash改变 天然的线程安全 知识来源&#xff1a; 【基础】String、StringB…

【云原生】Docker 详解(二):Docker 架构及工作原理

Docker 详解&#xff08;二&#xff09;&#xff1a;Docker 架构及工作原理 Docker 在运行时分为 Docker 引擎&#xff08;服务端守护进程&#xff09; 和 客户端工具&#xff0c;我们日常使用各种 docker 命令&#xff0c;其实就是在使用 客户端工具 与 Docker 引擎 进行交互。…

【Kafka】2.在SpringBoot中使用官方原生java版Kafka客户端

目 录 1. 新建一个消息生产者2. 新建一个消息消费者3. 测 试 在开始之前&#xff0c;需要先做点准备工作&#xff0c;用 IDEA 新建一个 Maven 项目&#xff0c;取名 kafka-study&#xff0c;然后删掉它的 src 目录&#xff0c;接着在 pom.xml 里面引入下面的依赖。这个项目的作…

[oneAPI] 手写数字识别-LSTM

[oneAPI] 手写数字识别-LSTM 手写数字识别参数与包加载数据模型训练过程结果 oneAPI 比赛&#xff1a;https://marketing.csdn.net/p/f3e44fbfe46c465f4d9d6c23e38e0517 Intel DevCloud for oneAPI&#xff1a;https://devcloud.intel.com/oneapi/get_started/aiAnalyticsToolk…

在本地搭建WAMP服务器并通过端口实现局域网访问(无需公网IP)

文章目录 前言1.Wamp服务器搭建1.1 Wamp下载和安装1.2 Wamp网页测试 2. Cpolar内网穿透的安装和注册2.1 本地网页发布2.2 Cpolar云端设置2.3 Cpolar本地设置 3. 公网访问测试4. 结语 前言 软件技术的发展日新月异&#xff0c;各种能方便我们生活、工作和娱乐的新软件层出不穷&a…