IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 【Linux】进程信号 -> 正文阅读

[系统运维]【Linux】进程信号

Linux信号

信号的概念

软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。

信号的分类

使用命令kill -l可以查看所有的信号。

请添加图片描述

信号一共有62种,其中131号信号是普通信号**,**3464号信号是实时信号,普通信号和实时信号各自都有31个,每个信号都有一个编号和一个宏定义名称:

请添加图片描述

信号的产生

通过键盘按键产生

当我们遇到程序死循环,或者程序执行到一半不想执行的情况,通常会按ctrl +c来终止这个进程,或者用ctrl+\也可以终止进程。

写一个死循环,运行程序

请添加图片描述

ctrl+c的本质其实是向进程发送2号信号SIGINT,ctrl+\是向进程发送3号信号SIGQUIT。

查看手册,发现2号信号和3号信号的默认处理方式有些不一样

请添加图片描述

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump

请添加图片描述

Core Dump核心转储

首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: ulimit -c 1024

  • 使用 ulimit -a命令查看当前资源限制的设定

请添加图片描述

第一行显示core文件的大小为0,即表示核心转储是被关闭的。

  • 使用ulimit -c size命令来设置core文件的大小

请添加图片描述

  • 再次使用ctrl+\终止这个进程,发现后面显示core dump

请添加图片描述

  • 当前目录下会多出一个文件core.pid,.后面就是被终止进程的pid

请添加图片描述

  • 使用core dump文件进行事后调试

写一个野指针访问

#include<stdio.h>
int main(){
	int *p = NULL;
	*p = 666;
	return 0;
}
  • 运行程序,就会出现段错误,程序崩溃,这时就会在目录下出现core文件

请添加图片描述

  • 接下来使用gdb调试这个程序,然后使用core-file core文件命令加载core文件,即可判断出该程序在终止时收到了11号信号,并且定位到了产生该错误的具体代码。

请添加图片描述

通过系统函数向进程发信号

  • kill接口

向指定进程发送指定信号,

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

参数

  • pid:要发送信号的进程pid
  • sig:要发送的信号

返回值

成功返回0

失败返回-1

使用命令行参数和kill接口实现一个进程,用来杀死指定进程。

#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<stdlib.h>
int main(int argc, char* argv[]) {
	if (argc == 3) {
		kill(atoi(argv[1]), atoi(argv[2]));
	}
	return 0;
}

请添加图片描述


  • raise接口

向调用进程发送指定信号

#include <signal.h>
int raise(int sig);

参数

sig:指定的信号

返回值

成功返回0

失败返回-1


  • abort函数

(库函数)给当前进程发送SIGABRT信号,使得当前进程异常终止

#include <stdlib.h>
void abort(void);
//就像exit函数一样,abort函数总是会成功的,所以没有返回值

没有参数,没有返回值,直接调用即可


通过软件条件产生信号

  • SIGPIPE信号

在学习管道的时候,我们知道关闭所有读端的时候,再往管道内写入数据,进程就会收到SIGPIPE信号被终止

  • SIGALRM信号

调用alarm接口可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

#include <unistd.h>
 unsigned int alarm(unsigned int seconds);

参数

  • seconds:多少秒之后发送SIGALRM信号

返回值

  • 若调用alarm函数前,进程已经设置了闹钟,则返回上一个闹钟时间的剩余时间,并且本次闹钟的设置会覆盖上一次闹钟的设置。
  • 如果调用alarm函数前,进程没有设置闹钟,则返回值为0。
#include<unistd.h>
int main(){
    alarm(5);
    sleep(100);
    return 0;
}

请添加图片描述


由硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,进程需要使用的地址都是通过页表映射到物理内存的,如果发生非法访问内存,有可能页表种就没有这个内存的映射信息,或者说没有对这块内存的访问权限,MMU会识别到页表访问异常,内核将这个异常解释为SIGSEGV信号发送给进程。

  • 非法访问内存
#include<stdio.h>
int main(){
	int *p = NULL;
	*p = 666;
	return 0;
}

请添加图片描述

  • 除0
#include<stdio.h>
int main(){
  	int a = 1/0;
    return 0;
}

请添加图片描述

由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。

信号的保存

在了解信号的保存方式之前,我们先了解一下信号的专业词汇

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

信号保存原理

信号是通知进程发生了异步事件,进程收到信号后,并不是立即处理的,而是先保存起来(未决),并且判断这个信号是不是被阻塞(屏蔽),等到进程从内核态切换为用户态的时候,才会处理信号(递达)。

那么信号在被递达之前,是怎么被保存的呢?信号的屏蔽又是怎么实现的?

信号产生后,进程会收到信号,在进程PCB中,有三个表分别用来记录信号的相关信息,其中pending表和block表是用位图来实现的,位图的每一位都代表一个信号的种类,而每一位的值代表状态。

  • pending

比特位的位置代表某一个信号,比特位的内容代表是否收到该信号。

  • block

比特位的位置代表某一个信号,比特位的内容代表该信号是否被阻塞

  • handler

handler其实是一个函数指针数组,下表就是代表几号信号,数组的内容也就是函数指针,指向的是该信号递达时的处理动作,(默认/忽略/自定义)

请添加图片描述

  • 上图的1号信号,pending位图数据为0,就是当前进程没有收到1号信号,block为0,进程收到1号信号不会被阻塞(屏蔽),handler为SIG_DFL,表示收到1号信号后的处理方式为默认信号处理程序。
  • 上图的2号信号,pending位图数据为1,表示当前进程收到2号信号,信号处于未决状态,等待递达,block为1,2号信号被屏蔽,在解除2号信号的屏蔽之前,未决的2号信号不会被递达,handler为SIG_IGN,收到2号信号的处理方式为忽略信号的处理程序
  • 上图的3号信号,pending位图数据为1,表示当前进程收到3号信号,信号处于未决状态,等待递达,block为0,3号信号不会被屏蔽,在进程内核态返回用户态时,3号信号会被递达,递达的方式是用户自定义处理

源码具体结构

请添加图片描述

上图的pending表和block表,都用一个相同的数据类型sigset_t来实现位图存储信息


sigset_t(信号集)

从上图来看,pending表和block表的位图结构,就是由这个sigset_t类型来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

信号集操作函数

由于每个平台的sigset_t信号集实现位图的方法不一定一样,所以我们不推荐对这个信号集直接进行修改,而是通过系统提供给我们的接口来修改sigset_t类型的变量。

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • sigemptyset接口

功能

初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。

参数

sigset_t类型的变量,传入要操作的信号集的指针

返回值

成功返回0,出错返回-1。


  • sigfillset接口

功能

初始化set所指向的信号集,使其中所有信号的对应bit位清零

参数

sigset_t类型的变量,传入要操作的信号集的指针

返回值

成功返回0,出错返回-1。


  • sigaddset接口

功能

初始化set所指向的信号集,使其中所有信号的对应bit位全部设置为1

参数

sigset_t类型的变量,传入要操作的信号集的指针

返回值

成功返回0,出错返回-1。


  • sigaddset接口

功能

设置set所指向的信号集,使其中所指定的信号bit位设置为1

参数

  • 第一个参数:sigset_t类型的变量,传入要操作的信号集的指针
  • 第二个参数:要设置的信号编号

返回值

成功返回0,出错返回-1。


  • sigdelset接口

功能

设置set所指向的信号集,使其中所指定的信号bit位设置为0

参数

  • 第一个参数:sigset_t类型的变量,传入要操作的信号集的指针
  • 第二个参数:要设置的信号编号

返回值

成功返回0,出错返回-1。


  • sigismember接口

功能

判断set所指向的信号集中是否包含指定的信号

参数

  • 第一个参数:sigset_t类型的变量,传入要操作的信号集的指针
  • 第二个参数:要设置的信号编号

返回值

包含则返回1不包含则返回0调用失败返回-1


注意

在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。

修改/获取进程信号的信号屏蔽字(sigprocmask接口)

上面的原理中说的block表,也叫做信号屏蔽字或者阻塞信号集

sigset_t以及操作函数都是对信号集这个变量进行操作,我们要真正修改或者获取进程信号阻塞的位图,就要调用sigprocmask接口。

  • sigprocmask接口
#include <signal.h>
int  sigprocmask(int how, const sigset_t *set, sigset_t* oldset);

参数

  • 第一个参数how,表示要对信号屏蔽字进行的操作类型
  • 第二个参数set,就是我们要设置的信号集的指针
  • 第三个参数oldset,是一个输出型参数,可以获取到当前进程的信号屏蔽字,保存到oldset中,用作备份

参数说明

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

how参数的可选值:

选项功能
SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask
SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask
SIG_SETMASK设置当前信号屏蔽字为set所指向的值,相当于mask=set

返回值

成功返回0,出错返回-1。

获取进程信号pending表(sigpending接口)

pending表中存放着当前进程未决信号集,只能获取数据,不能修改。

  • sigpending接口
#include <signal.h>
int sigpending(sigset_t *set);

参数

sigset_t类型的变量,传入要操作的信号集的指针

返回值

成功返回0,出错返回-1。

实现一个小程序,打印进程收到信号的pending表位图信息

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

void show_pending(sigset_t* pending) {
    int sig = 1;
    for (; sig <= 31; ++sig) {
        if (sigismember(pending, sig)) {
            printf("1");
        }
        else {
            printf("0");
        }
    }
    printf("\n");
}
//自定义信号递达
void sighandler(int sig) {
    printf("catch a sig:%d\n", sig);
}




int main() {
    //设置3个信号集,分别为未决信号集,阻塞信号集,备份阻塞信号集
    sigset_t pending;
    sigset_t block, oldblock;
    //初始化阻塞信号集
    sigemptyset(&block);
    sigemptyset(&oldblock);
    //设置信号集2号信号bit位为1
    sigaddset(&block, 2);
    //屏蔽(阻塞)2号信号
    sigprocmask(SIG_SETMASK, &block, &oldblock);
    //自定义捕捉2号信号
    signal(2, sighandler);


    int count = 0;
    while (1) {
        //初始化未决信号集
        sigemptyset(&pending);
        //获取当前进程未决信号集
        sigpending(&pending);
        show_pending(&pending);
        sleep(1);
        count++;
        if (count == 10) {
            sigprocmask(SIG_SETMASK, &oldblock, NULL);
            printf("恢复原来阻塞信号集\n");
        }
    }
    return 0;
}

请添加图片描述

信号的处理

信号的处理动作也叫信号的递达

信号的递达方式一般有三种,默认,忽略,捕捉

而信号的递达并不是收到信号就立即递达,而是先保存,等到进程由内核态转为用户态的时候,才递达信号。

默认递达

信号的默认递达方式使用man 7 signal 可以查看手册

请添加图片描述

大多数信号的默认递达方式是结束进程

忽略递达

忽略递达的意思是收到了该信号,对这个信号的处理方式为忽略,也就是不理这个信号,是信号递达的一种方式

屏蔽信号则是把这个信号的屏蔽字修改为1,信号处于未决状态,信号没有递达

自定义捕捉

信号的另一种递达方式叫做捕捉,就是用户自己设置了收到信号后的处理方式,如果信号不被阻塞的话,收到信号时,就会执行用户自定义的代码

signal接口

前面已经使用过signal接口,就是自定义信号的处理方式,也叫信号的捕捉。

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数

  • 第一个参数signum,表示需要自定义的信号编号
  • 第二个参数handler,表示自己自定义函数的地址

返回值

该函数返回一个指向信号处理程序的指针。出错则返回SIG_ERR(-1)。

使用

#include<signal.h>
#include<stdio.h>
#include<unistd.h>
void sighandler(int sig) {
	printf("catch a signal: %d\n", sig);
}
int main() {
	while (1) {
		signal(2, sighandler);
	}
	return 0;
}

请添加图片描述

按下ctrl+c向进程发送2号信号,信号被捕捉,输出如上图

sigaction接口

sigaction和signal类似,都是自定义捕捉指定信号。

#include <signal.h>
 int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

参数

  • 第一个参数signum,表示需要自定义的信号编号
  • 第二个参数cat,若act指针非空,则根据act修改该信号的处理动作。
  • 第三个参数oldact,若oldact指针非空,则通过oldact传出该信号原来的处理动作

第二个参数和第三个参数是一个结构体指针变量,这个结构体指针如下:

struct sigaction {
	void     (*sa_handler)(int);
	void     (*sa_sigaction)(int, siginfo_t*, void*);
	sigset_t   sa_mask;
	int        sa_flags;
	void     (*sa_restorer)(void);
};

要自定义信号处理,我们需要对第一个成员 sa_handler进行设置

  • 将sa_handler赋值为常数SIG_IGN传给sigaction函数,表示忽略信号。
  • 将sa_handler赋值为常数SIG_DFL传给sigaction函数,表示执行系统默认动作。
  • 将sa_handler赋值为一个函数指针,表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数。

将第三个成员sa_mask信号集初始化为空

将第四个成员sa_flags设置为0

返回值

成功返回0,出错返回-1。

使用

#include<signal.h>
#include<stdio.h>
#include<unistd.h>
void sighandler(int sig) {
	printf("catch a signal: %d\n", sig);
}
int main() {
	while (1) {
		struct sigaction act, oact;
		act.sa_flags = 0;
		sigemptyset(&act.sa_mask);
		act.sa_handler = sighandler;
        
		sigaction(2, &act, &oact);
	}
	return 0;
}

请添加图片描述

信号的捕捉流程(内核实现)

内核空间和用户空间

每个进程都有自己的地址空间,进程地址空间分为用户空间和内核空间

  • 进程的代码和数据都存放在用户空间,通过用户级页表映射到物理内存中
  • 操作系统的系统调用等的代码和数据存放在内核空间,通过内核级页表映射到物理内存中

用户空间是每个进程私有的,数据和代码都独有一份,内核空间的代码和数据,操作系统只有一份,所有进程都使用这一份。

请添加图片描述

内核态和用户态
  • 当进程在执行操作系统的代码(系统调用等)时,进程处于内核态,处于内核态时,进程的权限大
  • 当进程在执行自己的代码时,处于用户态,用户态的权限很小

在几种情况下,进程会从用户态切换到内核态

  • 执行系统调用
  • 产生中断/异常

在几种情况下,进程会从内核态切换到用户态

  • 执行完系统调用返回时
  • 中断/异常处理完时

从用户态切换到内核态叫做陷入内核,一般来说,内核态只会执行系统调用代码,而用户态只会执行用户代码,理论上来说内核态的权限足够大,可以执行用户态的代码,但是为了内核安全,不会进行这样的操作。

内核实现信号捕捉

进程收到操作系统发送的信号时,不会立即处理,而是等到由内核态切换为用户态时才会处理。

在执行代码过程中,可能会遇到一些问题,异常或者系统调用,进程就会陷入内核,在处理完异常或者执行完系统调用时,会先检查PCB中的pending位图

如果发现有未决的信号,就会查看block位图,看这个信号是否被阻塞

如果发现未决信号,并且信号没有被阻塞,就会让信号递达

  • 如果这时信号的递达方式是默认,那么信号递达大概率会直接结束进程
  • 如果这时信号的递达方式是忽略,那么信号递达之后会清除pending位图的未决状态,然会返回用户态,从主控流程切换到内核态的地方继续执行

请添加图片描述

  • 如果这时信号的递达方式是自定义捕捉,那么信号在执行递达时会返回用户态执行处理函数,执行完通过特殊的系统调用sigreturn再次陷入内核,清除pending位图,完成信号递达,再次检查pending位图,没有新的信号要递达,直接返回用户态,继续执行主控流程代码

请添加图片描述

sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

用户态和内核态的几次切换过程,可以用一个简易图来记

请添加图片描述

与直线的一次相交就代表一次状态切换

可重入函数和不可重入函数

请添加图片描述

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数

如果一个函数符合以下条件之一则是不可重入的:

调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile关键字

对于一个普通变量,为提高存取速率,编译器会先将变量的值存储在一个寄存器中,以后再取变量值时,就存寄存器中取出。

但是用voliate修饰的变量,就说明这个变量会发生意向不到的改变。也就是说,优化器每次在读取该值时,不会假设这个值了,每次都会小心的在读取这个变量的值,而不是在寄存器中取保留的备份。

举一个例子

#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig){
	printf("chage flag 0 to 1\n");
	flag = 1;
}
int main(){
	signal(2, handler);
	while (!flag);
	printf("process quit normal\n");
	return 0;
}

进程运行时,一直死循环,当按下ctrl+c向进程发送2号信号时,信号被捕捉,就会把flog设为1,死循环就会结束

请添加图片描述

如果我们把编译器的优化等级设置为O2编译执行,再看看结果

请添加图片描述

按下ctrl+c进程没有退出,说明flag被编译器优化了,存到了寄存器中,按下ctrl+c,flag只会修改内存中flag的值,寄存器中的flag还是0,所以进程一直继续

给全局变量加上关键字volatile再次编译执行

请添加图片描述请添加图片描述

volatile 作用保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-04-04 12:51:11  更:2022-04-04 12:54:58 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 22:33:17-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码