iOS 必知必会 - APNs篇

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 必知必会 - APNs篇相关的知识,希望对你有一定的参考价值。

参考技术A

导语:

由于移动设备内存、CPU、电量的局限性,ios 不允许 APP 的进程常驻后台(事实上可以申请后台运行一段时间,最长约 10 分钟),这样当用户主动杀掉 APP,或者 APP 进入后台超过约定时长时,就意味着该 APP 进程的结束。这在很大程度上保障了前台 APP 的流畅性,也延长了手机的使用时长,获得了较好的用户体验。但是这也意味着,服务器无法主动和用户交互(如推送实时消息等)。为了解决这个限制,苹果推出了 APNs,允许设备和服务器分别与苹果的推送通知服务器保持长连接状态。

iOS 的通知分为本地通知和远程通知。本地通知是由本地应用触发的,一般是基于时间的一种通知形式,如闹钟、待办事件等的提醒。远程通知是由开发商通过自己的服务器推送的一种通知形式,而 APNs 就是远程通知功能的核心。

关于远程推送,记住以下两点就够了:

这里就很清楚了,其实 APNs 的本质就是 服务器和客户端之间的中介 。当服务器需要给客户端推送消息时,先将消息发送给苹果服务器,再由苹果服务器找到对应设备推送下去。

那为什么还要走中介,不直接发送呢?因为这样做一个设备(即所有 APP )只需要和苹果的服务器建立一条长连接,而不需要每个 APP 都和服务器建立一条长连接。

可能有些人还是不太明白 APNs 的意义,觉得也只是将多个长连接变成了统一的一个长连接而已,有必要那么做吗?

很有必要!

我们来看下 android 的推送现状就明白了。

Android 事实上也有类似于 APNs 的一套用于推送的服务,简称 GCM,即 Google Cloud Messaging。但由于 GCM 需要谷歌服务器的支持,在国内由于「墙」的原因基本不能使用。这下就热闹了,国内出现了一大堆第三方推送服务商,如华为推送、小米推送、极光推送等。APP 通过集成这些推送服务来实现推送功能,而这些推送服务为了保持自己的长连接不被杀死,采用了各种保活、唤醒手段,这也是 Android 手机使用不流畅的真凶。之前也有看到「 工信部要求国内安卓统一消息推送标准 」的新闻,工信部都这么重视,可见统一推送的意义非凡。

想要了解具体区别,可以参考这篇文章 「 国内 90%以上的 iOS 开发者,对 APNs 的认识都是错的 」。
不言而喻,当然是尽早升级 HTTP/2 协议了。

参考:

(完)

Linux 编程之信号篇:异常监控必知必会

文章目录

为什么要了解信号

信号是 UNIX 系统进程管理非常重要的一环,下面这些场景都需要通过信号实现:

  1. 进程接收内核的通知(比如内核通知进程 用户输入了信息)
  2. 系统终止一个进程
  3. 管理父子进程(比如通知父进程子进程退出了)
  4. 进程间通信

信号在 Android 系统中的地位也非常重要。通过了解信号,我们可以实现对应用运行状态的监听,最实际的用途,就是监听应用发生崩溃、ANR 并上报。

因此,我们有必要掌握信号的使用和基本原理,从而对关注的信号进行处理。

什么是信号

信号是一种软中断, 是一种通知方式。

当收到内核或者其他进程发送的信号后,接收信号会从当前执行的代码转移到之前注册的信号处理函数(如果注册了的话),当信号处理函数执行完成后,恢复执行之前的代码(如果没有退出的话)。

上一句话括号里之所以有“如果注册了的话”,是因为在接收到信号后,进程可以有多种选择:

  1. 忽略
  2. 使用默认的处理函数
  3. 自己注册一个信号处理函数

当进程通过 syscall 指定要忽略某些信号后,再接收到这些信号,会直接无视,继续执行;如果是没有忽略的信号,就会执行默认或者注册的处理函数。

另外,我们也可以通过信号传递一些数据,这一般用于进程间通信。

信号有哪些

在 UNIX 系统中,信号有大概 31 个,具体定义在 asm/signal.h 中(使用 adb shell kill -l 也能看到这些信号),这里我们仅看下常见的几种:

  • SIGKILL(9)/SIGSTOP(19):被系统强制杀死/停止**(不能被捕获或者忽略)**
  • SIGQUIT(3): Android 进程 ANR 时会收到这个信号
  • SIGABRT(6): 主动 abort() 后触发,
  • SIGALRM(14):主动 alarm() 后触发
  • SIGSEGV(11):段错误,访问无效内存或者无权访问内存
  • SIGBUS(7):硬件或对齐错误
  • SIGFPE(8):算数异常,包括溢出、除以 0 等
  • SIGCHLD(17):子进程被终止时,内核给父进程发送
  • SIPROF(27):向没有读取端的管道写入,一般用作调试

可以看到,信号的名称都是 SIGxxx,另外每个信号也有唯一的整数值,在我们注册相关监听时,都是使用整数,名称只是为了直观理解其类型。

还有一个值为 0 的空信号,这个仅用于测试是否有权限发送信号。

如何发送/捕获信号

老 API:

  • kill()
  • signal()

推荐 API:

  • sigqueue()
  • sigaction()

推荐理由:可以发送的信息更多

发送

kill

我们可以通过 adb 使用 kill: adb shell kill -9 pid

在代码中使用 kill 的话,也很简单,它的函数签名:

int kill(pid_t __pid, int __signal);
  • pid > 0,会给该进程发送信号
  • pid = 0,会给调用进程的【进程组中的每个进程】发送信号
  • pid = -1,会向调用进程发送信号
  • pid < -1,会给 pid 绝对值的进程组发送信号

返回值:0 成功;-1 失败。

  • killpg
  • raise

raise

raise 用于给自己发信号,就是简化版的 kill:

int raise(int __signal);
//等于 kill(getpid(), signo);

返回值:0 成功;其他值失败。

killpg

给一个进程组发送信号:

int killpg(int __pgrp, int __signal);

等同于 kill(-1* pgrp, signal)

需要注意的是,发送信号,需要权限,用户只能给 UID 相同的进程发送信号,除非是 ROOT 设备。

sigqueue

sigqueue 可以发送待附加信息的信号,方法签名如下:

int sigqueue(pid_t __pid, int __signal, const union sigval __value)

typedef union sigval 
  int sival_int;
  void __user * sival_ptr;
 sigval_t;

sigqueue 只能给指定的进程发送信号,不能发给一个进程组。

使用案例:

    char* msg = static_cast<char *>(calloc(12, sizeof(char)));
    msg = "zhangshixin";

    union sigval value;
    value.sival_ptr = msg;
    int ret = sigqueue(getpid(), signo, value);

    LOG(" send signal by sigqueue, signo:%d , ret: %d, msg: %s", signo, ret, msg);

监听

signal

signal 函数比较简单,第一个参数是要监听的信号,第二个参数是一个信号处理函数:

#if __ANDROID_API__ >= __ANDROID_API_L__
sighandler_t signal(int __signal, sighandler_t __handler)

通过宏可以看出,signal 只支持 5.0 级以上版本的设备。

写一个例子:

//信号处理函数必须返回 void
void my_signal_handler(int signo) 
    LOG("my_signal_handler called, sig_no: %d", signo);


void my_signal_handler_2(int signo) 
    LOG("my_signal_handler_2 called, sig_no: %d, signame: %s, name by sys_siglist: %s",
            signo, strsignal(signo), sys_siglist[signo]);


void old_signal_listen_test() 
    //监听 SIGPROF,返回该信号之前注册的信号处理函数
    auto ret = signal(SIGPROF, my_signal_handler);
    ret = signal(SIGPROF, my_signal_handler_2);
    LOG("register signal handler ret: %p, my_signal_handler: %p", ret, my_signal_handler);

    if (ret == SIG_ERR) 
        fprintf(stderr, "register signal handler failed!");
        return;
    

    //忽略
    signal(SIGFPE, SIG_IGN);


    //只在接收到可捕获的信号时才返回,返回值返回 -1
    int pause_ret = pause();
    LOG("pause returned, ret: %d", pause_ret);

    //使用默认处理
    ret = signal(SIGPROF, SIG_DFL);
    LOG("set SIGPROF use default handler, ret: %p", ret);

上面的例子中,我们做了三件事:

  1. 注册了两次信号处理函数,然后打印出第二次注册时,返回值的地址
  2. 忽略了 SIGFPE 信号
  3. 调用 pause,阻塞当前代码
  4. 在收到信号后,设置 SIGPROF 信号继续使用默认处理方式

通过运行结果可以看到,的确是返回的第一次注册的信号处理函数的地址:

zsx_linux: register signal handler ret: 0x788d755264, my_signal_handler: 0x788d755264

注册成功后,我们可以在 adb shell 中发送一个 SIGPROF 信号进行测试(需要 ROOT 手机):

admin@C02ZL010LVCK debug % adb shell
ursa:/ $ top | grep performance
25263 shell        20   0 8.8M 2.2M 1.8M S  0.0   0.0   0:00.02 grep performance
25236 u0_a124      10 -10 4.3G 122M  73M S  0.0   1.5   0:00.57 top.shixinzhang.performance
ursa:/ $ su
:/ # kill
kill
usage:	kill [-s signame | -signum | -signame]  job | pid | pgrp  ...
	kill -l [exit_status ...]
1|:/ # kill -27 25236
kill -27 25236

kill 命令的格式:kill -signum pid

发送后,我们在信号处理函数里的日志就如期打印了:

zsx_linux: my_signal_handler_2 called, sig_no: 27, signame: Profiling timer expired, name by sys_siglist: Profiling timer expired
zsx_linux: pause returned, ret: -1
zsx_linux: set SIGPROF use default handler, ret: 0x788d756360

上面的测试代码还有 2 个细节:

  1. pause() 是 Linux 提供的测试信号 API,调用它会立刻阻塞,在接收到可捕获的信号时才返回
  2. strsignal(signo)sys_siglist[signo] 可以获取信号的名称,一般推荐用后者

需要注意的是,不是所有信号都可以捕获并处理的,比如注册 SIGKILL SIGSTOP 就直接会返回 SIG_ERR;注册 SIGUSR1 虽然 OK,但发送这些信号,也不会执行我们注册的信号处理函数 (可能被阻塞了)。

sigaction

https://man7.org/linux/man-pages/man2/sigaction.2.html

sigaction 比 signal 更为强大,它可以获取到信号发送者的信息和进程状态。

#include <signal.h>

int sigaction(int signum, const struct sigaction *restrict act,
                     struct sigaction *restrict oldact);

第一个参数是要注册的信号,第二个参数要信号处理相关的结构体,第三个参数是之前设置的信号处理行为。

sigaction 结构体内容如下:

struct sigaction 
  union 
    sighandler_t sa_handler;	//和 signal 一样的信号处理函数
    void (*sa_sigaction)(int, struct siginfo*, void*);		//可以获取到更详细信息的信号处理函数
  ;
  sigset_t sa_mask;
  int sa_flags;
  void (*sa_restorer)(void);
;

sa_flags 支持这些参数:

能额外获取到的信息:

__SIGINFO struct  int si_signo; int si_errno; int si_code; union __sifields _sifields; 
union __sifields 
  struct 
    __kernel_pid_t _pid;
    __kernel_uid32_t _uid;
   _kill;
  struct 
    __kernel_pid_t _pid;
    __kernel_uid32_t _uid;
    sigval_t _sigval;
   _rt;
  //...
;

__sifields 内容非常多,这里我做了精简,重点关注信号发送者的信息、信号产生的原因和传递的数据。

使用例子:

void my_sa_sigaction(int signo, struct siginfo* info, void* context) 
    LOG("my_sa_sigaction called ! signo: %d", signo);

    auto _signo = info->si_signo;
    //错误原因
    auto sig_code = info->si_code;

    auto killer_pid = info->_sifields._kill._pid;
    auto killer_uid = info->_sifields._kill._uid;

    auto syscall = info->_sifields._sigsys._syscall;

    LOG("_signo: %d", _signo);
    LOG("sig_code: %d, send by sigqueue? %d", sig_code, sig_code == SI_QUEUE);
    LOG("killer_pid: %d , killer_uid: %d", killer_pid, killer_uid);
    LOG("syscall: %d", syscall);

    auto value = context;
    LOG("value: %s", value);

    //获取发送的数据
    sigval_t sig_value = info->_sifields._rt._sigval;
    LOG("sig_value %s", sig_value.sival_ptr);



//比 signal 更为强大,可以获取的信息更多
void sigaction_test() 
    struct sigaction sig_action;
    //设置信号的处理方式,SA_SIGINFO 表示使用信息更多的信号处理函数
    sig_action.sa_flags = SA_SIGINFO;

    //设置执行信号处理函数时,要阻塞的信号集(避免重入)
    //目前正在被处理的信号,也是被阻塞的
//    sig_action.sa_mask = SIGPROF;

    sig_action.sa_sigaction = my_sa_sigaction;

    //成功时返回 0
    int ret = sigaction(SIGPROF, &sig_action, nullptr);
    if (ret) 
        LOG("sigaction set failed! ");
    
    LOG("set sigaction ret: %d", ret);

使用 sigqueue 发送信号后,输出结果

zsx_linux: my_sa_sigaction called ! signo: 27
zsx_linux: _signo: 27
zsx_linux: sig_code: -1, send by sigqueue? 1
zsx_linux: killer_pid: 2919 , killer_uid: 10124
zsx_linux: sig_value zhangshixin

信号处理函数的注意事项

实现信号处理函数,有 2 点需要注意:

  1. 最好不要访问、修改全局数据
  2. 不调用不可重入函数

第一点建议的原因:由于信号处理函数是异步调用,可能在程序执行数据读写的过程中被切换到信号处理函数,如果在信号处理函数中访问、修改这部分数据,会导致结果不可预期。

第二点中,可重入的函数就是说多次调用不会有异常,建议在信号处理函数中仅使用可重入的函数。

Linux 信号可重入的安全函数列表见 https://man7.org/linux/man-pages/man7/signal-safety.7.html

信号阻塞

有些情况下,在用户程序或者信号处理函数中,可能需要对挂起某些信号,即使收到这些信号,也暂不处理,继续执行任务,等执行完当前任务,再开始接收信号。

Linux 提供了这种机制,称为信号的阻塞(Block)和解除阻塞(UnBlock)。

Linux 中管保存当前进程阻塞的信号数据,叫做“信号掩码”,signal mask。

获取、修改信号掩码的 API 是 sigprocmask:

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

https://man7.org/linux/man-pages/man2/sigprocmask.2.html

我们可以通过构造一个信号集(即多个信号),然后调用 sigprocmask

  1. 如果传入的参数是 SIG_BLOCK,就是把信号集里的信号,也都阻塞了(其余之前设置的不变)
  2. 如果传入的参数是 SIG_UNBLOCK,就是把信号集里的信号,解除阻塞(其余之前设置的不变)
  3. 如果传入的参数是 SIG_SETMASK,就是粗暴地把进程的阻塞的信号,设置成当前的信号集合(之前设置的不要了)

可以通过传入一个空的 set,获取 old_set,然后判断当前进程是否阻塞某个信号:

void process_block_signal_mask_test() 
    //信号集
    sigset_t set, fill_set, old_set;
    //1.初始化空信号集,把所有信号都排除
    int ret = sigemptyset(&set);
    LOG("sigemptyset ret: %d", ret);

    //把所有信号都包括在内
    sigfillset(&fill_set);

    //添加 SIGQUIT 到信号集
//    ret = sigaddset(&set, SIGQUIT);
//    LOG("sigaddset ret: %d", ret);

    ret = sigismember(&set, SIGQUIT);
    LOG("check sigismember, sig: %d ret: %d", SIGQUIT, ret);;
    ret = sigismember(&fill_set, SIGQUIT);
    LOG("check [fill_set] sigismember, sig: %d ret: %d", SIGQUIT, ret);;

    //2.可以传入空 set,只为看当前的信号掩码有哪些
    ret = sigprocmask(SIG_BLOCK, &set, &old_set);
    //检查是否默认阻塞 SIGQUIT,答案是是的
    LOG("check process mask, SIGQUIT ismember: %d", sigismember(&old_set, SIGQUIT));

    sigset_t pending_set;
    //3.获取当前进程待处理的信号
    if (sigpending(&pending_set)) 
        LOG("sigpending failed");
    
    LOG("sigpending ret: %d, pending_set: %ld", ret, pending_set);

    //4.恢复旧的信号掩码,这个函数和 pause 一样会阻塞,直到收到信号
    ret = sigsuspend(&old_set);
    LOG("sigsuspend ret: %d, old_set: %ld", ret, old_set);

输出结果:

zsx_linux: sigemptyset ret: 0
//空集合,不包含 SIGQUIT
zsx_linux: check sigismember, sig: 3 ret: 0
//满集合,包含 SIGQUIT
zsx_linux: check [fill_set] sigismember, sig: 3 ret: 1
//进程初始化时,把 SIGQUIT 信号屏蔽了
zsx_linux: check process mask, SIGQUIT ismember: 1

上面我们看到,在我的 demo app 里,默认就把 SIGQUIT 屏蔽了,具体实现代码在 Runtime::init 里,会调用到 Runtime::BlockSignals:

void Runtime::BlockSignals() 
  SignalSet signals;
  signals.Add(SIGPIPE);
  // SIGQUIT is used to dump the runtime's state (including stack traces).
  signals.Add(SIGQUIT);
  // SIGUSR1 is used to initiate a GC.
  signals.Add(SIGUSR1);
  signals.Block();



void Block() 
  if (pthread_sigmask64(SIG_BLOCK, &set_, nullptr) != 0) 
    PLOG(FATAL) << "pthread_sigmask failed";
  

可以看到,Runtime::BlockSignals 会调用 pthread_sigmask64 设置这三个信号阻塞,这里也解答了为什么前面我们注册了 SIGUSR1 信号但在测试时没有收到该信号。

pthread_sigmask 和 sigprocmask 基本一样,但 POSIX.1 标准里没有定义 sigprocmask 在多线程环境下的行为,所以可能在不同的系统上实现有些差异。
一般建议使用 pthread_sigmask

注意:前面提到无法注册的 SIGKILL 和 SIGSTOP,同样无法被阻塞。系统强制执行,无法修改。

我们可以通过 sigpending 获取当前进程被阻塞了、待处理的信号列表:

int sigpending(sigset_t* __set);

总结

通过本文,我们可以对 Linux 的信号机制有个基本的认识,知道信号的发送和注册方式,了解阻塞信号的方法,以及通过信号传递数据。

Thanks

  • 《Linux 系统编程》
  • https://man7.org/linux/man-pages/man7/signal.7.html
  • https://man7.org/linux/man-pages/man2/sigaction.2.html
  • https://man7.org/linux/man-pages/man7/signal-safety.7.html
  • https://man7.org/linux/man-pages/man3/pthread_sigmask.3.html

以上是关于iOS 必知必会 - APNs篇的主要内容,如果未能解决你的问题,请参考以下文章

MySQL必知必会 第7-9章

SQL必知必会(第五版)

软件测试基础必知必会

分库分表,必知必会!!

Linux 编程之信号篇:异常监控必知必会

必知必会 -一文搞定理解RPC