Android开发—Linux 信号

信号概念与信号处理器的作用

信号本身,其实是对事件发生时对进程的一种通知机制,当然,这种机制并不是只限于Linux系统,其他很多类Unix系统也有,信号的初衷只是通知,比如当硬件异常时,可由硬件本身的错误检测通知到内核,再由内核告诉相关的进程,又或者是,进程间可通过信号本身,去传递一些特有的消息。但是我们在日常开发中经常能够看到,比如nativecrash是由某个信号导致的,比如常见的SIGABRT/SIGSEGV等。但是这个表述,其实是不太准确的,大部分信号(这里特指除了SIGKILL/SIGSTOP)本身其实跟进程crash/exit,本身其实是没有必然关系的,这点需要注意,也是非常容易尝试误会的点。比如我们说,收到了SIGSEGV就会导致进程退出,其实,这只是一个默认的行为,记住,是默认的行为!下面我们列举一下常见的信号默认行为

信号 默认行为
SIGKILL 确保杀死进程,无法更改默认行为
SIGPIPE 管道相关断开,默认杀死进程
SIGSTOP 确保进程停止,无法更改默认行为,常用于debugger
SIGSEGV 无效内存引用,默认杀死进程
SIGABRT 中止进程,android上行为默认也是杀死
SIGQUIT 默认杀死进程,linux上是终端退出,而android用来产生anr

可以 点击 “此处” 即可 免费获取 学习资料 以及 更多Android学习笔记+源码解析+面试视频

看到这里,我们可能会有疑惑,好像各个信号其实都差不多都是杀死进程。还有就是SIGQUIT,明明android发生了anr会抛出SIGQUIT,但是也不是说进程也会被杀死呀!其实呢,上面列举了这些,都是信号本身的默认行为罢了,我们是可以改变默认行为的,比如通过系统调用signal(),或者sigaction()注册一个信号处理器,其实就会把一个信号从默认行为变成了自定义行为,如果说自定义行为里面没有进程退出的调用,比如exit,那么,及时当前进程收到了信号,也是不会崩溃的。当然这里指的是可更改默认行为的信号,除了SIGKILL与SIGSTOP,我们都可以通过上述的信号处理调用更改,我们举个例子

Java_com_example_signal_MainActivity_throwNativeCrash(JNIEnv *env, jobject thiz) {// 向自身发送一个信号raise(SIGABRT);__android_log_print(ANDROID_LOG_INFO, "hello", "%s", "qwe");}

当我们通过一个jni调用,调用到throwNativeCrash方法时,里面通过一个raise调用(意思是向当前进程本身发送一个信号),如果默认我们什么也不处理,那么就会导致进程中断退出,表现的形式就是app闪退。但是,假如我们加入了一个信号处理函数(添加信号处理函数一般有signal与sigaction)这里我们介绍一下sigaction

int sigaction(int __signal, const struct sigaction* __new_action, struct sigaction* __old_action);

sigaction接受3个参数,第一个是需要添加信号处理器的信号,第二个是一个sigaction的结构体的指针,用于设置当前信号的新信号处理器,第三个也是一个sigaction的结构体的指针,用于返回之前的信号处理器,如果有的话。

接着我们介绍sigaction结构体

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

union是c语言的一个概念,意味着sa_handler与函数sa_sigaction只取其中一个,当sa_flags包含SA_SIGINFO时,就调用sa_sigaction函数,默认就是调用sa_handler函数。

如果我们对SIGABRT设置了一个信号处理函数,即使SigFunc(收到信号要调用的函数)什么也做,进程也不会退出!

struct sigaction sigc;
sigc.sa_sigaction = SigFunc;
sigemptyset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO;
sigaction(SIGABRT, &sigc, nullptr);

这里非常关键啦!只要我们设置了信号处理函数,那么允许默认行为变更的信号的默认行为就会被改变,即使你的信号处理器什么也没干!

到这里,我们应该就能明白了,信号处理器其实就是更改信号默认行为的一种手段,也是对信号自定义处理的一些手段,这也是为什么我们一些apm框架,会通过监听更改信号处理器后,然后通过old_action重新再把信号抛出给默认处理器的原因,因为信号处理器只能设置一个,如果不重新调用old_action,那么app默认行为就会被改变(即产生了错误也不crash)。

这里可能给大家埋下了一个黑暗的想法,如果我们都把所有信号都注册一遍,就算我们信号处理器啥都不干,是不是就实现了native crash == 0 ? 好家伙,一般情况下,还真是,但是正常的错误已经是产生了,即使我们通过信号处理函数绕过了crash行为,但是当错误实在不可控了,系统就会发送SIGKILL/SIGSTOP的信号,而两个信号是无法通过信号处理器去进行修改的,这样造成的问题是,以后app crash了,也就无法定位出根本的堆栈,说不定会造成一个无法解决的BUG!这点是非常需要注意的!

信号接收进程的行为

上面我们说了一大堆,其实只是信号本身的默认行为,但是对于接收信号的进程来说,却有着多种情况,信号你可以发,接不接受,那是进程可控的。

当信号到达后,进程可表现以下几种方式

  • 忽略信号,内核将丢弃该信号,信号对进程不产生任何影响
  • 终止进程,也就是执行信号本身的一些,默认行为
  • 进程停止,比如debug通过信号控制
  • 恢复执行,比如通过信号恢复之前停止的进程

进程也可以通过有无设置信号处理器,将信号处理器定义为以下情况

  • 设置信号默认行为
  • 忽略信号
  • 自定义信号处理器

下面我们来讲一下,如何设置信号处理器的上诉行为

自定义信号处理器

我们上面也说过,我们可以通过,sigaction,设置自定义的信号处理器,比如

struct sigaction sigc;
sigc.sa_sigaction = SigFunc;
sigemptyset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO;sigaction(SIGABRT, &sigc, nullptr);

这里SigFunc是一个函数,函数定义为

void (*sa_sigaction)(int, struct siginfo*, void*);

即可,里面就可以处理自定义的一些逻辑

void SigFunc(int sig_num, siginfo *info, void *ptr) {比如打log
}

默认行为

我们可以通过SIG_DFL这个宏定义,设置信号处理器的默认行为,比如设置SIGABRT默认行为,这个宏需要放在sa_handler处理,意味着我们就不能在sa_flags加上SA_SIGINFO标记,而是采取默认的处理方式

struct sigaction sigc;
sigc.sa_handler = SIG_DFL;
sigfillset(&sigc.sa_mask);
sigc.sa_flags = SA_RESTART;
... 
sigaction(SIGABRT, &sigc, nullptr);

忽略信号

与默认行为相似,我们也有一个忽略信号的宏定义,则是SIG_IGN,我们也可以一样设置在sa_handler中

struct sigaction sigc;
sigc.sa_handler = SIG_IGN;
sigc.sa_flags = SA_RESTART;
sigfillset(&sigc.sa_mask);
...
sigaction(SIGABRT, &sigc, nullptr);

这里还不忘多提一句,当设置SIG_IGN,那么内核就会把该信号丢弃,这也意味值,进程丝毫不感知该信号,这也就意味着,如果发生异常了,虽然进程不会crash,但是错误足够大时同样会产生SIGKILL去终止进程,虽然忽略信号在一些情况可以避免一些潜在的信号错误的默认处理让进程不crash,但是也给bug排查带来困难

信号阻塞 – 掩码

我们前文已经说了,信号会由内核投递给每个需要的进程,我们说的阻塞,其实就是Linux内核内部给每个进程维护一个信号掩码,其实就是一个信号数组,如果发送给进程的信号位于掩码里面,内核就暂时阻塞该信号对进程的传递,直到进程通过调用通知内核移除为止,此时信号会被重新投递

使用sigprocmask调用,能够通知内核在信号掩码中添加/删除信号

int sigprocmask(int __how, const sigset_t* __new_set, sigset_t* __old_set);
  • 第一个参数how,有如下取之,SIG_BLOCK ,将__new_set参数里面的信号集添加到当前信号集中,当前信号掩码集合结果就是new_set&old_set。SIG_UNBLOCK,移除当前信号掩码集合中new_set所设置的信号,当前信号掩码集合结果就是new_set&~old_set。SIG_SETMASK,将new_set所指向的信号集合设置为当前的信号掩码集合,当前信号掩码集合结果就是new_set
  • new_set 与old_set都是一个结构体为sigset_t的指针,我们常用以下方式添加一个信号到set中
sigset_t blockset;
sigemptyset(&blockset);
sigaddset(&blockset,SIGBUS);

当然sigprocmask是属于进程级别的阻塞,我们还可以用pthread_sigmask指定某个线程独立去修改掩码

int pthread_sigmask(int __how, const sigset_t* __new_set, sigset_t* __old_set);

比如常见apm需要监听ANR产生的SIGQUIT信号时,需要解除当前线程中的SIGQUIT掩码(可见android系统应用线程创建时,会把SIGQUIT加入到掩码集合中,这部分之前文章讲过,这里就不详述)

sigemptyset(&mask);
sigaddset(&mask, SIGQUIT);
if (0 != pthread_sigmask(SIG_UNBLOCK, &mask, &old)) {//
}

发送信号方式

我们有以下发送信号的方式

int raise(int __signal); 调用者自身
int kill(pid_t __pid, int __signal); 指定进程/进程组
int killpg(int __pgrp, int __signal); 指定进程组
int tgkill(int __tgid, int __tid, int __signal);发送信号给线程/线程组

当然,还有我们用pthread_create 创建时,也可以直接指定pthread_t针对线程发起信号

int pthread_kill(pthread_t __pthread, int __signal);

上面的调用其实就如同字面所示,不过值得注意的是,当进程调用raise的时候,信号会在raise返回前就被发出,因此需要注意,同时由于不需要进程id,所以我们常用raise去模拟发出信号相关的动作

针对线程的信号发出,使用场景也很多,比如在apm中,如果我们想要获取anr之后的trace文件,当捕获完SIGQUIT后可以通过tgkill发出信号给SignalCatcher线程

其他信号补充

这一小节是对信号的补充

同步信号监听

我们通过sigaction进行的信号监听,也被称为异步信号监听,那么有没有同步的信号监听呢?

有的

int sigwait(const sigset_t* __set, int* __signal);

我们可以通过sigwait等相关调用,去执行一个同步等待,比如SignalCatcher线程会一直通过sigwait去等待SIGQUIT信号

在这里插入图片描述

abort()

我们这里还特别介绍了abort()这个系统调用,因为它在android源码中频繁用到,abort调用时会产生SIGABRT信号。那么这个调用有什么特别之处吗?还记得我们上文说的传递给进程的信号是有可能被阻塞或者被忽略的,但是abort()调用却不受影响,Linux遵从SUSV3的要求,abort调用必须终止进程(具体实现就是,在SIGABRT信号处理器结束执行时,会把相应信号处理还原成默认信号处理器,如有),除非进程注册了SIGABRT的信号处理器且处理函数还未被返回(完成后也一定会终止进程),因此,我们能够监听到abort系统调用产生的SIGABRT信号,但是如果信号处理函数正常执行完,就会立即终止进程。但是!这里有趣的是,非局部跳转(非本地跳转 setjmp等)是可以抵消abort产生的影响的,非常强大!

android系统对于信号的处理

我们都知道,android系统运行在Linux内核中,其实算是内核的一个“大应用”(可以认为androix虚拟机是linux操作系统的一个程序),那么我们在android系统上运行的我们自己的应用程序,定义的信号处理器会被第一时间执行到吗(地位等同于android系统吗),非常有趣!答案是,我们部分信号,注意是部分,其实是“二手”信号,这个信号其实是由内核 – android虚拟机 – 应用,经过了这么一层转换关系的。

我们可以在FaultManager中看到,这是android虚拟机信号初始化的起点(值得注意的是,这里以android13源码为分析,每个版本有些差异,但是大体流程相同)

void FaultManager::Init() {CHECK(!initialized_);sigset_t mask;sigfillset(&mask);sigdelset(&mask, SIGABRT);sigdelset(&mask, SIGBUS);sigdelset(&mask, SIGFPE);sigdelset(&mask, SIGILL);sigdelset(&mask, SIGSEGV);SigchainAction sa = {.sc_sigaction = art_fault_handler,.sc_mask = mask,.sc_flags = 0UL,};// 添加信号处理器AddSpecialSignalHandlerFn(SIGSEGV, &sa);// Notify the kernel that we intend to use a specific `membarrier()` command.int result = art::membarrier(MembarrierCommand::kRegisterPrivateExpedited);if (result != 0) {LOG(WARNING) << "FaultHandler: MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED failed: "<< errno << " " << strerror(errno);}{MutexLock lock(Thread::Current(), generated_code_ranges_lock_);for (size_t i = 0; i != kNumLocalGeneratedCodeRanges; ++i) {GeneratedCodeRange* next = (i + 1u != kNumLocalGeneratedCodeRanges)? &generated_code_ranges_storage_[i + 1u]: nullptr;generated_code_ranges_storage_[i].next.store(next, std::memory_order_relaxed);generated_code_ranges_storage_[i].start = nullptr;generated_code_ranges_storage_[i].size = 0u;}free_generated_code_ranges_ = generated_code_ranges_storage_;}initialized_ = true;
}

extern "C" void AddSpecialSignalHandlerFn(int signal, SigchainAction* sa) {InitializeSignalChain();if (signal <= 0 || signal >= _NSIG) {fatal("Invalid signal %d", signal);}// Set the managed_handler.chains[signal].AddSpecialHandler(sa);chains[signal].Claim(signal);
}

这里一件事,就是找到sigaction与sigprocmask这两个符号,并放入了相关的结构体


__attribute__((constructor)) static void InitializeSignalChain() {static std::once_flag once;std::call_once(once, []() {lookup_libc_symbol(&linked_sigaction, sigaction, "sigaction");lookup_libc_symbol(&linked_sigprocmask, sigprocmask, "sigprocmask");#if defined(__BIONIC__)lookup_libc_symbol(&linked_sigaction64, sigaction64, "sigaction64");lookup_libc_symbol(&linked_sigprocmask64, sigprocmask64, "sigprocmask64");
#endif});
}

这里的目的其实就是,通过FaultManager,确保了虚拟机能够第一时间先收到感兴趣的信号,先让虚拟机进行处理,之后有需要再发给我们应用定义的信号处理器,之所以需要找到sigaction与sigprocmask,其实就是采用了hook思想,android应用层调用的sigaction会被虚拟机统一替换成自定义的“sigaction” 处理,注意这里的区别,这个不是Linux调用的sigaction

extern "C" int sigaction(int signal, const struct sigaction* new_action,struct sigaction* old_action) {InitializeSignalChain();return __sigaction(signal, new_action, old_action, linked_sigaction);
}
template <typename SigactionType>
static int __sigaction(int signal, const SigactionType* new_action,SigactionType* old_action,int (*linked)(int, const SigactionType*,SigactionType*)) {
if (is_signal_hook_debuggable) {return 0;
}// If this signal has been claimed as a signal chain, record the user's
// action but don't pass it on to the kernel.
// Note that we check that the signal number is in range here.  An out of range signal
// number should behave exactly as the libc sigaction.
if (signal <= 0 || signal >= _NSIG) {errno = EINVAL;return -1;
}if (chains[signal].IsClaimed()) {
SigactionType saved_action = chains[signal].GetAction<SigactionType>();
if (new_action != nullptr) {
真正对真的sigaction预处理chains[signal].SetAction(new_action);
}
if (old_action != nullptr) {*old_action = saved_action;
}
return 0;
}// Will only get here if the signal chain has not been claimed.  We want
// to pass the sigaction on to the kernel via the real sigaction in libc.
return linked(signal, new_action, old_action); 调用真正的Linux sigaction的函数
}
void SetAction(const SigactionType* new_action) {if constexpr (std::is_same_v<decltype(action_), SigactionType>) {action_ = *new_action;} else {action_.sa_flags = new_action->sa_flags;action_.sa_handler = new_action->sa_handler;
#if defined(SA_RESTORER)action_.sa_restorer = new_action->sa_restorer;
#endifsigemptyset(&action_.sa_mask);memcpy(&action_.sa_mask, &new_action->sa_mask,std::min(sizeof(action_.sa_mask), sizeof(new_action->sa_mask)));}action_.sa_flags &= kernel_supported_flags_;
}

最后完成了内核 – android虚拟机 – 应用 这么一个信号传递机制,当遇到需要虚拟机先处理的信号时,比如SIGSEGV,就会通过FaultManager::HandleFault去处理,通过预先注册的各个Handler去处理

bool FaultManager::HandleFault(int sig, siginfo_t* info, void* context) {if (VLOG_IS_ON(signals)) {PrintSignalInfo(VLOG_STREAM(signals) << "Handling fault:" << "\n", info);}#ifdef TEST_NESTED_SIGNAL// Simulate a crash in a handler.raise(SIGSEGV);
#endifif (IsInGeneratedCode(info, context)) {VLOG(signals) << "in generated code, looking for handler";for (const auto& handler : generated_code_handlers_) {VLOG(signals) << "invoking Action on handler " << handler;if (handler->Action(sig, info, context)) {// We have handled a signal so it's time to return from the// signal handler to the appropriate place.return true;}}}// We hit a signal we didn't handle.  This might be something for which// we can give more information about so call all registered handlers to// see if it is.if (HandleFaultByOtherHandlers(sig, info, context)) {return true;}// Set a breakpoint in this function to catch unhandled signals.art_sigsegv_fault();return false;
}

可以 点击 “此处” 即可 免费获取 学习资料 以及 更多Android学习笔记+源码解析+面试视频

查看全文

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

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

相关文章:

在这里插入图片描述

Android开发—Linux 信号

信号概念与信号处理器的作用
信号本身,其实是对事件发生时对进程的一种通知机制,当然,这种机制并不是只限于Linux系统,其他很多类Unix系统也有,信号的初衷只是通知,比如当硬件异常时,可由硬件本……

【Java面试八股文宝典之MongoDB篇】备战2023 查缺补漏 你越早准备 越早成功!!!——Day18

大家好,我是陶然同学,软件工程大三即将实习。认识我的朋友们知道,我是科班出身,学的还行,但是对面试掌握不够,所以我将用这100多天更新Java面试题🙃🙃。 不敢苟同,相信大……

Qt关于Model/View大数据量刷新的处理经验

目录引言可能的问题点及优化方法初始化耗时过长更新item耗时过长显示耗时过长模型过多引言
前段时间有同学咨询关于大数据量的Model刷新时卡顿的优化方案,通过评论留言的方式回答了一部分,但感觉不够全面。因为这个是之前项目重点解决的问题&#xff0c……

Java自学Promise的理解和应用,好程序员分享Promise模式实现

一、Promise小白怎么用?从一个故事开始吧
1、先来一段废话故事
您是一名在古老迷失城市中探险的冒险家。您身处一间装饰华丽的房间中,四周布满了古老的壁画和雕塑。您发现有两个通道分别通向不同的方向,分别是:一个黑暗的通道和……

easy_aruco包代码逻辑梳理

easy_aruco包代码逻辑梳理
easy_aruco源码
OpenCV tutorial
请配合源码阅读该文章
ROS 节点的配置 <node pkg"tf2_ros" type"static_transform_publisher" name"board_mid"args"0.1 0.0 0.0 0.0 0.0 0.0 1.0 board board_mid"&……

Android进程间大数据通信:LocalSocket

作者:BennuCTech 前言
说起Android进行间通信,大家第一时间会想到AIDL,但是由于Binder机制的限制,AIDL无法传输超大数据。
那么我们如何在进程间传输大数据呢?
Android中给我们提供了另外一个机制:Local……

C++标准输入流输出流的控制符

*# 控制符(操作符)
dec 数值的基数为10。 hex 数值的基数为16。 Oct 数值的基数为8。 setfill(x) 设置填充字符,其中x为要填充的字符常量或者字符变量,要填充的字符个数可由控制符setw设置。 setprecision(x) 对于一般……

跳出计算机领域的迷茫之坑

清明节的第一个晚上,思绪重重,不过对于我来说,这已经不是一两次了。说到底,还是因为对这个兴业的极度迷茫,以及对碌碌无为的自己的不甘心。我本身就不是一个自律力很强的人,尽管我也是爱思考,而……

如何在学习计算机这条路上越走越稳健?

相对来说,每一个热爱计算机并且渴望学更多相关领域知识的人,都有一颗“速成”心理,也就是急于求成,殊不知在学知识的过程中,是最忌讳这样的心理。因为怀着这样一种“速成”心理,除了会影响我们学知识的真正……

特殊的递归问题

例题: 从键盘输入一个正整数,利用递归法打印输出这个数。 答案如下:
#include<iostream>
using namespace std;
int main()
{void f(int x);int x;cout << "please input a integer:";cin >> x;f(x);
}
void f(……

JavaScript【六】JavaScript中的字符串(String)

文章目录🌟前言🌟字符串(String)🌟单引号和双引号的区别🌟属性🌟 length :字符串的长度🌟 方法🌟 str.charAt(index);🌟 str.charCodeAt(index);🌟 String.fromCharCode(……

获取文件MD5小案例(未拆分文件)

文章目录前端获取MD5后端获取MD5前端获取MD5
1、引入js
<script src"js/spark-md5.min.js" type"text/javascript"></script>注:spark-md5库GitHub链接 2、这里是一个按钮和被隐藏调的<input/>标签 <body><button……

Java 进阶(15)线程安全集合

CopyOnWriteArrayList
线程安全的ArrayList,加强版读写分离。
写有锁,读⽆锁,读写之间不阻塞,优于读写锁。
写⼊时,先copy⼀个容器副本、再添加新元素,最后替换引⽤。
使⽤⽅式与ArrayList⽆异。
示例……

HR:面试官最爱问的linux问题,看看你能答对多少

文章目录摘要Linux的文件系统是什么样子的?如何访问和管理文件和目录?如何在Linux中查看和管理进程?如何使用Linux命令行工具来查看系统资源使用情况?如何配置Linux系统的网络设置?如何使用Linux的cron任务调度器来执行……

vscode开发常用的工具栏选项,查看源码技巧以及【vscode常用的快捷键】

一、开发常用的工具栏选项
1、当前打开的文件快速在左侧资源树中定位: 其实打开了当前的文件已经有在左侧资源树木定位了,只是颜色比较浅 2、打开太多文件的时候,可以关闭 3、设置查看当前类或文件的结构 OUTLINE
相当于idea 查看当前类或接……

数据要素化条件之一:原始性

随着技术的发展,计算机不仅成为人类处理信息的工具,而且逐渐地具有自主处理数据的能力,出现了替代人工的数据智能技术。数据智能的大规模使用需要关于同一分析对象或同一问题的、来源于不同数据源的海量数据。这种数据必须是针对特定对象的记……

【面试题 高逼格利用 类实现加法】编写代码, 实现多线程数组求和.

编写代码, 实现多线程数组求和.关键1. 数组的初始化关键2. 奇偶的相加import java.util.Random;public class Thread_2533 {public static void main(String[] args) throws InterruptedException {// 记录开始时间long start System.currentTimeMillis();// 1. 给定一个很长的……

一个python训练

美国:28:麻省理工学院,斯坦福大学,哈佛大学,加州理工学院,芝加哥大学,普林斯顿大学,宾夕法尼亚大学,耶鲁大学,康奈尔大学,哥伦比亚大学,密歇根大学安娜堡分校,约翰霍普金斯大学,西北大学,加州大学伯克利分校,纽约大学,加州大学洛杉矶分校,杜克大学,卡内基梅隆大学,加州大学圣地……

Mybatis03学习笔记

目录 使用注解开发
设置事务自动提交
mybatis运行原理
注解CRUD
lombok使用(偷懒神器,大神都不建议使用)
复杂查询环境(多对一)
复杂查询环境(一对多)
动态sql环境搭建
动态sql常用标签……

设置或取得c# NumericUpDown 编辑框值的方法,(注意:不是Value值)

本人在C#开发中使用到了NumericUpDown控件,但是发现该控件不能直接控制显示值,经研究得到下面的解决办法
NumericUpDown由于是由多个控件组合而来的控件,其中包含一个类似TextBox的控件,若想取得或改变其中的值要使用如下方法
N……

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注