【Linux从青铜到王者】进程信号

news2024/9/25 3:21:13

———————————————————————————————————————————

信号入门

在了解信号之前有许多要理解的相关概念

我们可以先通过一个生活例子来初步认识一下信号

1.生活角度的信号

  • 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”。
  • 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取"。
  • 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取"。
  • 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)。
  • 快递到来的整个过程,你不能准确断定快递员什么时候给你打电话,所以该过程对你来讲是异步的

2.同步与异步,阻塞与非阻塞

同步就是发出一个调用后,当这个调用没有得到结果的时候,该调用就一直不返回

而异步则相反,当发出一个调用后,不管这个调用有没有取得结果,直接就返回了,后面通过状态,和通知来告诉调用者,或者通过回调函数来调用这个调用

阻塞和非阻塞关注的是程序在等待调用结构时的状态

阻塞调用指的是当获得调用结果之前,当前进程会被挂起

非阻塞调用指的是在没有获得调用结果之前,当前进程不会被挂起,会继续执行

举个通俗的例子:
你打电话问书店老板有没有 《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,〞我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)
而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了 (不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调

如果是阻塞调用,那么当你问完书店老板以后,你会在电话前一直等,直到书店老板给你回电;而如果是非阻塞调用,那么当你问完以后就会去干其他事情了,例如刷剧打游戏等等

3.进程的注意事项

一个bash只能有一个前台进程,可以有多个后台进程

Ctrl-C 产生的信号只能发给前台进程。只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号

前台进程不能被暂停,一旦被暂停就被自动放到后台进程中去

一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程

前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步的

4.信号的概念

什么叫做信号?信号其实是向目标进程发送通知信息的一种机制

信号的本质:其实就是用软件来模拟中断的过程——软中断

中断是什么呢?本质是电信号。这里涉及部分硬件原理,大概理解一下即可

当你在键盘敲ctrl+c的时候,键盘这个外设就产生了电信号,通过总线发送给中断控制器,再通过8259将电信号转换为中断号(0~n),被OS捕捉

这里外设产生电信号被转换为中断号的过程一般叫硬中断

5.信号介绍

信号分为普通信号和实时信号,本篇主要讨论普通信号

可以通过kill -l命令查看系统定义的命令,每个信号的具体使用可以使用man -7 signal命令

总共有62个信号,1-31为普通信号(大部分为终止进程),其余为实时信号

可以看到每个信号由一个序号+一个名字组成,通过以前所学这里的名字我们可以大概推测出来是宏,类似于#define SIGHUP1

这里的序号就是中断号,而在进程中会存在一张函数指针数组(中断向量表),进程通过序号(数组下标)可以调用不同的函数

这里大概了解一下即可,下文会详解

6.如何全面理解信号

下文将从信号的产生,保存和捕捉处理三个大部分来详解一下

其中保存和捕捉处理十分重要

信号的产生

1.通过终端硬件产生

其实就是上文所讲的通过键盘发送信号,常见的有ctrl+c,向前台进程发送2号新号,ctrl+z(默认暂停进程),ctrl+·默认终止进程

敲下键盘-》外设产生电信号->转变为中断号-》被os拿到发送给进程-》每个进程都有自己的一个中断向量表,中断号与数组下标强相关,通过中断号调用对应的函数

还是下面这张图

注意:这里被os写入进程十分重要,因为os是软硬件的管理者,无论信号的产生有多少种方式,最后只能被os拿到,然后发送给进程

2.通过系统调用产生

kill命令是通过kill函数完成的,kill函数可以给一个指定的进程发送指定的信号

可以通过kill函数来实现自己的,这里需要用到之前学的命令行参数

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

void Usage(char*s)
{
    printf("Usage:%s pid signo\n",s);
}
//kill -9
int main(int argc,char*argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        return 1;
    }
    pid_t pid=atoi(argv[1]);
    int signo=atoi(argv[2]);
    kill(pid,signo);
    return 0;
}

除了kill函数,还有raise和abort

int raise(int sig)

raise函数用于给当前进程发送sig信号,成功返回1,不成功返回0

void handler(int signo)
{
    printf("get a signal:%d\n",signo);
}
int main()
{
    signal(2,handler);
    while(1)
    {
        sleep(1);
        raise(2);
    }
    return 0;
}

void abort(void)

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

void handler(int signo)
{
    printf("get a signal:%d\n",signo);
}

int main()
{
    signal(6,handler);//对信号自定义捕捉
    while(1)
    {
        sleep(1);
        abort();
    }
    return 0;
}

abort函数是一个无参无返回值的函数,就是向进程自己发送6号信号

即使6号信号被自定义捕捉后不退出进程,使用abort函数后总是会退出进程

总结:exit是终止正常结束的进程,abor是终止异常结束的进程,终止方法为向进程发送6号信号,即使6号信号被自定义捕捉后没有执行退出逻辑操作,调用abor函数后总是能退出

3.通过软件条件产生

SIGPIPE信号和闹钟SIGALRM

SIGPIPE信号(13号信号)实际上是一种由软件条件产生的信号,我们都知道管道遵从一定的规则

假如管道的读端关闭,写端还在写数据的时候,此时管道已经没有存在的必要了,写端就会收到SIGPIPE信号从而被终止

unsigned int alarm(unsigned int seconds);

调用alarm函数可以让os在seconds秒之后给当前进程发送SIGALARM信号,SIGALARM信号的默认动作是终止进程

4.通过硬件异常

当进程中出现除零错误或者野指针和越界访问问题,为什么程序会崩溃呢?因为os识别到相关错误向进程发送对应信号使其终止

那么是如何识别除零错误或者野指针和越界访问问题的呢?

先说除零错误。我们知道cpu中有一堆的寄存器,当寄存器进行算术的时候,有些状态寄存器的值也要改变。在这些状态寄存器中肯定有某个寄存器的某个比特位表示除数是否为0,一旦检测出来除数为0,那么对应的硬件信息就会被os所识别到,然后包装成软件信息发送信号给当前进程

野指针和越界访问问题

我们都知道当虚拟地址向物理地址转换的时候,是通过页表转换的,页表属于一种软件映射关系

而实际上从虚拟地址到物理地址映射的时候还有一个硬件叫做MMU,它是一种负责cpu内存访问请求的计算机硬件

当需要进行虚拟地址到物理地址的映射时,我们先将页表的左侧的虚拟地址导给MMU,然后MMU会计算出对应的物理地址,我们再通过这个物理地址进行相应的访问。

既然MMU是硬件,所以就有对应的状态信息。当我们要访问不属于自己的虚拟地址空间的时候,MMU在转换的时候就会出现错误,从而被os识别,发送信号给进程,让进程终止

总结:程序之所以会崩溃,就是进行错误操作的时候一些硬件信息被os捕捉到,然后包装成软件信息向进程发送信号,从而终止进程

信号的保存

首先要理解一下几个概念

实际执行信号的处理动作,称为信号递达
信号从产生到递达之间的状态,称为信号未决

进程可以选择阻塞某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
需要注意的是,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后的一种处理动作。

三张表

信号被发送给一个进程之后,进程可能不是立即执行的,那么进程就要保存这个信号,怎么保存呢?通过位图保存

在一个进程中都会存在三张表,block位图(信号屏蔽字,阻塞信号集)表示对应信号是否被阻塞,pending位图表示该信号是否未决,还有一个handler表——函数指针数组,保存默认的处理方法

所以之前说的os发送信号给进程,其实就是向进程对应的位图写入数据

假如我向一个进程发送2号信号,该进程的pending表的二号位置就会变为1,此时2号信号信号未决,直到信号被处理之前,该位置一直为1;如果2号信号被写入pending表但是对应的block也被写入,就是信号被阻塞,此时不执行对应的默认处理方法,直到阻塞被解除

如果是忽略,那么就是对应的pending被写为1,block写为0。先将pending写为0,执行空方法,也就什么都不做

假设在进程在执行其他的信号方法的期间发送多个2号信号,pending的2号位置仍为1,当之前的方法处理完之后,2号的对应方法只被执行一次(其他系统可能不一样)

总结一下:
在block位图中,比特位的位置代表某一个信号,比特位的内容代表该信号是否被阻塞。
在pending位图中,比特位的位置代表某一个信号,比特位的内容代表是否收到该信号。
handler表本质上是一个函数指针数组,数组的下标代表某一个信号,数组的内容代表该信号递达时的处理动作,处理动作包括默认、忽略以及自定义。
block、pending和handler这三张表的每一个位置是一一对应的。

sigset_t及信号集操作函数

sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态。

其实就是在系统中被定义的位图,我们直接把其当做c语言中的变量使用即可

修改位图就要修改其中的比特位,必然涉及大量的位操作,对于使用者的体验肯定是不好的,所以就有了信号集操作函数

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);  

sigemptyset函数:初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
sigfillset函数:初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
sigaddset函数:在set所指向的信号集中添加某种有效信号。
sigdelset函数:在set所指向的信号集中删除某种有效信号。
sigismember函数:判断在set所指向的信号集中是否包含某种信号,若包含则返回1,不包含则返回0,调用失败返回-1。

sigprocmask

sigprocmask函数可以用于读取或更改进程的信号屏蔽字(阻塞信号集),

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数说明:

如果oset是非空指针,则读取进程当前的信号屏蔽字通过oset参数传出。
如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。

sigpending

sigpending函数可以用于读取进程的未决信号集,

int sigpending(sigset_t *set);

关于以上接口大家可以自己去用用,这里贴个小实验给大家了解一下大概的用法

先用上面的函数将2号信号进行阻塞,使用kill命令或组合按键向进程发送2号信号,此时2号信号会一直被阻塞,并一直处于未决状态,使用sigpending函数获取当前进程的pending信号集进行验证。

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

void printPending(sigset_t *pending)
{
	int i = 1;
	for (i = 1; i <= 31; i++){
		if (sigismember(pending, i)){
			printf("1 ");
		}
		else{
			printf("0 ");
		}
	}
	printf("\n");
}
void handler(int signo)
{
	printf("handler signo:%d\n", signo);
}
int main()
{
	signal(2, handler);
	sigset_t set, oset;
	sigemptyset(&set);
	sigemptyset(&oset);

	sigaddset(&set, 2); //SIGINT
	sigprocmask(SIG_SETMASK, &set, &oset); //阻塞2号信号

	sigset_t pending;
	sigemptyset(&pending);

	int count = 0;
	while (1){
		sigpending(&pending); //获取pending
		printPending(&pending); //打印pending位图(1表示未决)
		sleep(1);
		count++;
		if (count == 20){
			sigprocmask(SIG_SETMASK, &oset, NULL); //恢复曾经的信号屏蔽字
			printf("恢复信号屏蔽字\n");
		}
	}
	return 0;
}

信号的捕捉

拿完快递后我们会在合适的时候打开,同理进程也会在合适的时候处理信号,是在什么时候呢?

从内核态返回到用户态的时候,进行信号的检测和处理

在了解什么是内核态和用户态前,我们要先理解一下什么是内核空间和用户空间

用户空间和内核空间

每一个进程都有自己的进程地址空间,该进程地址空间由内核空间(3~4GB)和用户空间(1~3GB)组成

内核空间所存放的都是操作系统的代码和数据,所有进程看到的都是一样的内容

用户空间存放当前进程的代码和数据,每个进程看到的内容是不一样的(父子进程除外)

用户空间通过用户级页表与物理内存之间建立映射关系
内核空间通过内核级页表与物理内存之间建立映射关系

内核级页表是全局的,每个进程都用该页表去物理内存找os的代码和数据

而用户级页表是每个进程一份的,每个进程都用该页表去物理内存找该进程的代码和数据

用户态和内核态

在之前学习权限的时候我们就知道代码的执行是有权限的,假如不给权限你就无法完成一件事情

内核态通常用来执行操作系统的代码,是一种权限非常高的状态。
用户态是一种用来执行普通用户代码的状态,是一种受监管的普通状态

系统调用背后,就包含了进程身份的转变

进程收到信号之后,并不是立即处理信号,而是在合适的时候,这里所说的合适的时候实际上就是指,从内核态切换回用户态的时候

从用户态切换为内核态通常有如下几种情况:

  • 需要进行系统调用时。
  • 当前进程的时间片到了,导致进程切换。
  • 产生异常、中断、陷阱等。

与之相对应,从内核态切换为用户态有如下几种情况:

  • 系统调用返回时。
  • 进程切换完毕。
  • 异常、中断、陷阱等处理完毕。

进程默认是在用户态的,而在调用系统调用的时候,就会从用户态切换成内核态,然后通过在内核空间里的虚拟地址,通过内核级页表和MMU去物理内存中找到相应的代码和数据并执行

当进程收到的信号是默认信号的时候,如果是默认动作,那么把相应的pending表的对应位置置为1 之后,就会去找在内核空间的handler表并执行对应的代码

画圈的地方就是状态切换的地方 

而如果信号被自定义捕捉的话,就要从内核态切换为用户态,去执行自定义的放法,执行完通过系统调用sigreturn返回到内核态

巧记

整体过程就是一个无穷符号!

如果有多个信号要处理,在处理完前面信号返回到内核态的时候,继续进行信号的检测,执行对应的方法,如此循环直到pending表为空,再返回到用户态,继续往下执行代码

为什么不能把自定义捕捉的函数放在内核空间中呢?

因为内核态处于很高的一种状态,有些用户态执行不了的方法它也能执行,为了防止该自定义函数用较高权限乱操作,例如删除数据库等操作,因为内核态的权限足够高可以支持它完成这项操作,所以要将自定义函数放在用户空间中,这样就能防止上面情况发生

os怎么知道该进程当前是处于用户态还是内核态的呢?

cpu中有相应的状态寄存器的某个位置可以标记,该位置可以被os识别并转换信息,例如0为用户态1为内核态,那么根据该位置的值就知道该进程是处于什么状态了

那么问题就来了,如果整个代码没有调用系统调用接口,该进程就不会切换成内核态,就不会进行信号的检测和处理了吗?

当然不是的!进程都是有相应的时间片的,一个进程的时间片到了cpu就要去调度下一个进程了,当前进程的时间片到了,导致进程切换也是要进入内核态的

总结

至此信号的讲解就结束了,本文从三个方面——信号的产生,保存和处理来进行分析,希望大家能对信号有个全面而又清晰的认识

本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流!希望大家多多点赞转发支持一下

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微服务架构SpringCloud(2)

热点参数限流 注&#xff1a;热点参数限流默认是对Springmvc资源无效&#xff1b; 隔离和降级 1.开启feign.sentinel.enabletrue 2.FeignClient(fallbackFactory) 3.创建一个类并实现FallbackFactory接口 4.加入依赖 <!--添加Sentienl依赖--><dependency><gro…

深度学习系列61:在CPU上运行大模型

1. 快速版 1.1 llamafile https://github.com/Mozilla-Ocho/llamafile 直接下载就可以用&#xff0c;链接为&#xff1a;https://huggingface.co/jartine/llava-v1.5-7B-GGUF/resolve/main/llava-v1.5-7b-q4.llamafile?downloadtrue 启动&#xff1a;./llava-v1.5-7b-q4.lla…

代码随想录刷题笔记-Day28

1. 重新安排行程 332. 重新安排行程https://leetcode.cn/problems/reconstruct-itinerary/给你一份航线列表 tickets &#xff0c;其中 tickets[i] [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。 所有这些机票都属于一个从 JFK&#xff08;肯…

Linux 运维:CentOS/RHEL防火墙和selinux设置

Linux 运维&#xff1a;CentOS/RHEL防火墙和selinux设置 一、防火墙常用管理命令1.1 CentOS/RHEL 7系统1.2 CentOS/RHEL 6系统 二、临时/永久关闭SELinux2.1 临时更改SELinux的执行模式2.2 永久更改SELinux的执行模式 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;…

【C++】在龙年拿捏智能指针

文章目录 1 :peach:为什么需要智能指针&#xff1f;:peach:2 :peach:内存泄漏:peach:2.1 :apple:什么是内存泄漏:apple:2.2 :apple:内存泄漏分类:apple:2.3 :apple:如何检测内存泄漏:apple:2.4:apple:如何避免内存泄漏:apple: 3 :peach:智能指针的使用及原理:peach:3.1 :apple:…

微服务间通信重构与服务治理笔记

父工程 依赖版本管理,但实际不引入依赖 pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation&…

vue svelte solid 虚拟滚动性能对比

前言 由于svelte solid 两大无虚拟DOM框架&#xff0c;由于其性能好&#xff0c;在前端越来越有影响力。 因此本次想要验证&#xff0c;这三个框架关于实现表格虚拟滚动的性能。 比较版本 vue3.4.21svelte4.2.12solid-js1.8.15 比较代码 这里使用了我的 stk-table-vue(np…

GIN与Echo:选择正确Go框架的指南

您是否在Go中构建Web应用&#xff1f;选择正确的框架至关重要&#xff01;GIN和Echo是两个热门选择&#xff0c;每个都有其优势和特点。本指南将详细介绍每个框架的特性、速度、社区热度以及它们各自擅长的项目类型。最后&#xff0c;您将能够为您的下一个Web项目选择完美的框架…

SpringBoot + Disruptor 实现特快高并发处理

使用Disruptor做消息队列&#xff0c;解决内存队列的延迟问题&#xff08;在性能测试中发现竟然与I/O操作处于同样的数量级&#xff09; 【基于 Disruptor 开发的系统单线程能支撑每秒 600 万订单】 核心概念&#xff1a; Ring Buffer 环形的缓冲区&#xff0c;从3.0版本开始…

SQL 查询一张卡的最新使用记录

SQL 查询一张卡的最新使用记录 1. 问题描述 1. 问题描述 一张卡&#xff0c;有一个底表记录这个卡的基本信息&#xff0c;还有一个使用卡的记录表&#xff0c;记录着&#xff0c;这张卡的使用记录&#xff0c;但我们要获取这张卡的最新使用记录&#xff0c;该如何写SQL呢&…

【Linux命令】fuser

fuser 使用文件或文件结构识别进程。 详细 fuser命令用于报告进程使用的文件和网络套接字。fuser命令列出了本地进程的进程号&#xff0c;哪些本地进程使用file&#xff0c;参数指定的本地或远程文件。 每个进程号后面都跟随一个字母&#xff0c;该字母指示进程如何使用该文…

Python实现CCI工具判断信号:股票技术分析的工具系列(5)

Python实现CCI工具判断信号&#xff1a;股票技术分析的工具系列&#xff08;5&#xff09; 介绍算法解释 代码rolling函数介绍完整代码data代码CCI.py 介绍 在股票技术分析中&#xff0c;CCI (商品路径指标&#xff09;是一种常用的技术指标&#xff0c;用于衡量股价是否处于超…

MATLAB知识点:使用for循环时需要注意的事项

​讲解视频&#xff1a;可以在bilibili搜索《MATLAB教程新手入门篇——数学建模清风主讲》。​ MATLAB教程新手入门篇&#xff08;数学建模清风主讲&#xff0c;适合零基础同学观看&#xff09;_哔哩哔哩_bilibili 节选自​第4章&#xff1a;MATLAB程序流程控制 在使用for循环…

HarmonyOS—HAP唯一性校验逻辑

HAP是应用安装的基本单位&#xff0c;在DevEco Studio工程目录中&#xff0c;一个HAP对应一个Module。应用打包时&#xff0c;每个Module生成一个.hap文件。 应用如果包含多个Module&#xff0c;在应用市场上架时&#xff0c;会将多个.hap文件打包成一个.app文件&#xff08;称…

第 125 场 LeetCode 双周赛题解

A 超过阈值的最少操作数 I 排序然后查找第一个大于等于 k 的元素所在的位置 class Solution { public:int minOperations(vector<int> &nums, int k) {sort(nums.begin(), nums.end());return lower_bound(nums.begin(), nums.end(), k) - nums.begin();} };B 超过阈…

数据结构(一)综述

一、常见的数据结构 数据结构优点缺点数组查找快增删慢链表增删快查找慢哈希表增删、查找都快数据散列&#xff0c;对存储空间有浪费栈顶部元素插入和取出快除顶部元素外&#xff0c;存取其他元素都很慢队列顶部元素取出和尾部元素插入快存取其他元素都很慢二叉树增删、查找都快…

自学高效备考2025年AMC8数学竞赛:2000-2024年AMC8真题解析

今天继续来随机看五道AMC8的真题和解析&#xff0c;根据实践经验&#xff0c;对于想了解或者加AMC8美国数学竞赛的孩子来说&#xff0c;吃透AMC8历年真题是备考最科学、最有效的方法之一。即使不参加AMC8竞赛&#xff0c;吃透了历年真题600道和背后的知识体系&#xff0c;那么小…

深入理解Tomcat

目录&#xff1a; TomcatTomcat简介如何下载tomcatTomcat工作原理Tomcat架构图Tomcat组件Server组件Service组件Connector组件Engine组件Host组件Context组件 配置虚拟主机(Host)配置Context Tomcat Tomcat简介 Tomcat服务器是Apache的一个开源免费的Web容器。它实现了JavaEE…

计算机网络-物理层-传输媒体

传输媒体的分类 导向型-同轴电缆 导向型-双绞线 导向型-光纤 非导向型

卡密交易系统 卡密社区SUP系统源码 分销系统平台 分销商城系统开发

卡密社区SUP系统总控源码主站分销系统功能源码 跟以前的卡盟那种控制端差不多总控可以给别人开通&#xff0c;分销&#xff0c;主站&#xff0c;类似自己做系统商一样&#xff0c;自助发卡&#xff0c;卡密交易系统。 搭建环境Nginx1.22 mysql 5.7 php8.1 rids 7.2 安装方法…