Android进程系列第一篇---进程基础

内容预览.png
概述:

本文主要讲解进程基础,更深入的认识有血有肉的进程,内容涉及进程控制块,信号,进程FD泄露等等。仅供参考,欢迎指正。

一、从Linux看进程到底是什么?

“进程四要素” —《Linux 内核源代码情景分析》描述如下:

  • 有一段程序供其执行
  • 拥有专用的系统堆栈空间
  • 在内核存在对应进程控制块
  • 拥有独立的用户存储空间

上面确实有点抽象,进程不仅仅是一段处在执行状态的程序,还包括还文件,信号,内存地址空间,CPU状态等等复杂的信息,更重要的是拥有独立的用户存储空间,这个是与线程的本质区别。在Linux内核中,进程是由一个task_struct 的结构体表示的,task_struct又称进程控制块,task_struct 定义在include/linux/sched.h文件中。
下面的代码给出了task_struct的一小部分,已经在后面写清楚了注释,这个结构体相对较大,在32位的机器上约1.7kb。

struct task_struct {
   volatile long state;    //进程的允许状态/* -1 unrunnable, 0 runnable, >0 stopped */ 
   void *stack;
   atomic_t usage;
   unsigned int flags; /* per process flags, defined below */
   pid_t pid;   //进程的pid
   pid_t tgid;//线程组的id
   struct task_struct *real_parent; /* real parent process (when being debugged)  ,如果创建它的父进程不再存在,则指向PID为1的init进程*/  
   struct task_struct *parent; /* parent process “养父进程”通常与real_parent值相同,当它终止时,必须向它的父进程发送信号*/  
   struct list_head children;  /* list of my children */该进程的孩子进程链表
   struct list_head sibling;   /* linkage in my parent's children list */ 该进程的兄弟进程链表
   struct list_head thread_group; //*线程链表 
   struct task_struct *group_leader;   /* threadgroup leader */该进程的线程组长
   struct timespec start_time;  //进程创建时间 
   struct fs_struct *fs;  //文件系统信息 
   struct files_struct *files;//打开的文件信息
   struct mm_struct *mm, *active_mm;//描述进程的地址空间,active_mm指向进程运行时所使用的内存描述符, 对于普通进程而言,这两个指针变量的值相同,但是内核线程kernel thread是没有进程地址空间的
   int prio, static_prio, normal_prio;//static_prio用于保存静态优先级,prio用于保存动态优先级
   unsigned int rt_priority;//用于保存实时优先级
   /* signal handlers */
   struct signal_struct *signal;
   struct sighand_struct *sighand;
}

state表示进程的状态定义如下,我们了解其中几种。

/*
* Task state bitmask. NOTE! These bits are also
* encoded in fs/proc/array.c: get_task_state().
*
* We have two separate sets of flags: task->state
* is about runnability, while task->exit_state are
* about the task exiting. Confusing, but this way
* modifying one set can't modify the other one by
* mistake.
*/
#define TASK_RUNNING        0
#define TASK_INTERRUPTIBLE  1
#define TASK_UNINTERRUPTIBLE    2
#define __TASK_STOPPED      4
#define __TASK_TRACED       8
/* in tsk->exit_state */
#define EXIT_DEAD       16
#define EXIT_ZOMBIE     32
#define EXIT_TRACE      (EXIT_ZOMBIE | EXIT_DEAD)
/* in tsk->state again */
#define TASK_DEAD       64
#define TASK_WAKEKILL       128
#define TASK_WAKING     256
#define TASK_PARKED     512
#define TASK_STATE_MAX      1024
状态 含义
TASK_RUNNING 0 表示进程要么正在执行,要么正要准备执行(就绪状态),正在等待cpu时间片的调度
TASK_INTERRUPTIBLE 1 进程因为等待一些条件而被挂起(阻塞)而所处的状态。这些条件主要包括:硬中断、资源、一些信号……,一旦等待的条件成立,进程就会从该状态迅速转化成为TASK_RUNNING状态,是一种可中断的等待状态
TASK_UNINTERRUPTIBLE 2 不可中断的等待状态
TASK_STOPPED 4 进程被停止执行,当进程接收到SIGSTOP、SIGTTIN、SIGTSTP或者SIGTTOU信号之后就会进入该状态
TASK_TRACED 8 表示进程被debugger等进程监视,进程执行被调试程序所停止,当一个进程被另外的进程所监视,每一个信号都会让进城进入该状态
EXIT_ZOMBIE 32 进程被终止,但是其父进程还没有使用wait()等系统调用来获知它的终止信息,此时进程成为僵尸进程
EXIT_DEAD 16 进程的最终状态

POSIX(Portable Operating System Interface for Computing System,准确地说是针对类Unix操作系统的标准化协议)规定一个进程内部的多个thread要共享一个PID,在很多情况下,进程都是动态分配一个 task_struct 表示,其实线程也是由一个task_struct 来表示的,所以task_struct具有双重身份,既可以作为进程对象,也可以作线程对象。这样,为了满足POSIX的线程规定,linux引入了线程组的概念,一个进程中的所有线程所共享的那个PID被称为线程组ID,也就是task struct中的tgid成员,因此,在linux kernel中,线程组ID(tgid,thread group id)就是传统意义的进程ID。对于getpid系统调用,linux内核返回了tgid。对于gettid系统调用,本意是要求返回线程ID,在linux内核中,返回了task struct的pid成员。简单来一句总结:POSIX的进程ID就是linux中的线程组ID。POSIX的线程ID也就是linux中的pid

小结:
1、进程控制块的数据结构,主要包含下列信息

a、进程的标志符,如pid,uid等
b、进程的状态,如state, exit_state, prio等
c、进程间的关系,如parent, children,sibling等
d、进程拥有的资源,如mm,fs,files,mm等
e、信号处理函数,如signal, sighand等

2、task_struct具有双重身份,线程和进程都是用task_struct表示,区别在于进程拥有独立的用户空间,而线程和其它线程是共享存储空间的。

二、进程进阶

2.1、 pid ,ppid ,tgid ,pgid ,sid 的理解

上面了解了进程的数据结构,我们可以通过下面两条命令来查看进程的信息,进一步加强进程相关标识的理解(pid ,ppid ,tgid ,pgid ,sid )

cat /proc/self/status 
cat /proc/self/stat 

拿头条App举例

jason:/ $ ps -ef |grep com.ss.android.article.news
u0_a159      10276  1112 87 15:19:50 ?    00:00:32 com.ss.android.article.news
u0_a159      10731  1112 1 15:19:56 ?     00:00:00 com.ss.android.article.news:pushservice
u0_a159      10794  1112 9 15:19:57 ?     00:00:02 com.ss.android.article.news:push
u0_a159      10953  1112 2 15:19:58 ?     00:00:00 com.ss.android.article.news:ad
shell        11198 11193 6 15:20:27 pts/0 00:00:00 grep com.ss.android.article.news
jason:/ $ cat /proc/10276/stat                                                                                                                                                                             
10276 (id.article.news) S 1112 1111 0 0 -1 1077936448 365859 144549 137 0 3783 3165 1004 406 16 -4 144 0 31842 2094268416 53986 18446744073709551615 1 1 0 0 0 0 4612 1 1073779960 0 0 0 17 2 0 0 0 0 0 0 0 0 0 0 0 0 0

每个参数意思为:
pid=10276 进程(包括轻量级进程,即线程)号
comm=id.article.news 应用程序或命令的名字
task_state=S 任务的状态,R:running, S:sleeping, D:disk T: stopped, T:tracing stop,Z:zombie, X:dead
ppid=1112 父进程ID
pgid=1111 Process Group ID 进程组 ID号
sid=0 该任务所在的会话组ID
TODO:内存中进程是怎么组织的
.....

pgid是什么:每个进程都会属于一个进程组(process group),每个进程组中可以包含多个进程。进程组会有一个进程组领导进程 (process group leader),领导进程的PID成为进程组的ID (process group ID, PGID),以识别进程组。

sid是什么:更进一步,在shell支持工作控制(job control)的前提下,多个进程组还可以构成一个会话 (session),sid标识会话id,Android中进程的sid基本都是0。

130|jason:/ $ cat /proc/10276/status                                                                                                                                                                       
Name:   id.article.news
State:  S (sleeping)
Tgid:   10276
Pid:    10276
PPid:   1112
TracerPid:  0
Uid:    10159   10159   10159   10159
Gid:    10159   10159   10159   10159
Ngid:   0
FDSize: 512
Groups: 3002 3003 9997 20159 50159 
VmPeak:  2078244 kB
VmSize:  2042672 kB
VmLck:         0 kB
VmPin:         0 kB
VmHWM:    234364 kB
VmRSS:    208068 kB
VmData:   335236 kB
VmStk:      8192 kB
VmExe:        20 kB
VmLib:    190804 kB
VmPTE:      1584 kB
VmPMD:        16 kB
VmSwap:        0 kB
Threads:    142

Tgid是什么:

  1. 对于一个多线程的进程来说,它实际上是一个进程组,每个线程在调用getpid()时获取到的是自己的tgid值,而线程组领头的那个领头线程的pid和tgid是相同的
  2. 对于独立进程,即没有使用线程的进程来说,它只有唯一一个线程,领头线程,所以它调用getpid()获取到的值就是它的pid

通过上面两个命令可以确认几个常见进程的关系

进程名称 pid ppid tgid pgid sid
init 1 0 1 1 0
kthreadd 2 0 2 0 0
zygote64 1111 1 1111 1111 0
zygote 1112 1 1112 1112 0
system_server 1735 1111 1735 1111 0
com.ss.android.article.news 10276 1112 10276 1111 0

或许用下面的图表示更直观


进程关系.png
  • 1号进程:init进程,用户空间的第一个进程,也是所有用户态进程的始祖进程,负责创建和管理各个native进程。也有0号线程,swapper进程、又叫idle进程,它创建了init进程和ktheadd进程。

  • 2号进程:kthreadd进程,内核线程的始祖进程,负责创建ksoftirqd/0等内核线程。

  • zygote进程:init创建的,有64位和32位两种,所有的java进程都是由他们孵化而来,他们是所有java进程的父进程。

  • system_server进程:Android的核心进程,1735号线程是其主线程

  • com.ss.android.article.news:普通的一个32位java进程。

从表格中列举的关系,可看到一个Android的App进程进程的创建过程,是由idle进程 -> init进程 -> zygote进程 -> system_server进程 -> App进程。

问题:64位下有两个zygote,zygote64和zygote。64位应用的父进程是zygote64,它的pgid也是zygote64的pid;32位应用的父进程是zygote,它的pgid却是zygote64的pid,如:com.ss.android.article.news的父进程是zygote(1112),但它的pgid是zygote64(1111),这是怎么回事呢?原来不管32位或64位的zygote,它在创建完子进程后,会调用setChildPgid()来改变子进程的pgid。

   private void setChildPgid(int pid) {
       // Try to move the new child into the peer's process group.
       try {
           Os.setpgid(pid, Os.getpgid(peer.getPid()));
       } catch (ErrnoException ex) {
           // This exception is expected in the case where
           // the peer is not in our session
           // TODO get rid of this log message in the case where
           // getsid(0) != getsid(peer.getPid())
           Log.i(TAG, "Zygote: setpgid failed. This is "
               + "normal if peer is not in our session");
       }
   }

peerpeer是socket的对端,也就是system_server。而system_server的pgid就是zygote64的pid。这样,所有zygote32创建出来的子进程,他们的pgid都是zygote64的pid了。

2.2、进程内存信息的查看

除了用上面的cat /proc/pid/status可以查看内存之外,也可以用 top -p pid。比如你写了一个占用内存的程序,想看看占用的内存对不对,就可以使用top -p pid。

top -p 11364
PID USER         PR  NI VIRT  RES  SHR S[%CPU] %MEM     TIME+ ARGS                                                                                                                                        
11364 root         20   0 1.7G  44K  24K S  0.0   0.0   2:14.29 ramServer 40

VIRT为1.7G,这个是虚拟内存,举个列子,malloc只是申请了虚拟空间,并没有占用物理内存。​只有使用时才会分配物理内存,如memset申请的那块区域时才会分配物理内存。实际占用的内存可以参考RES的大小。

2.3、进程的fd泄露问题

进程的fd泄露问题一直是一个很难搞的问题,先看一段system_server_crash的trace吧

1970-10-11 20:47:05 SYSTEM_RESTART (text, 342 bytes)
Build: Xiaomi/scorpio/scorpio:7.0/NRD90M/7.7.24:user/release-keys
Hardware: QC_Reference_Phone
Revision: 0
Bootloader: unknown
Radio: TH20c1.9-0711_1858_6c2ad98
Kernel: Linux version 3.18.31-perf-g0bf156d-00846-gadfe339 (builder@c3-miui-ota-bd28.bj) (gcc version 4.9 20150123 (prerelease) (GCC) ) #1 SMP PREEMPT Mon Jul 24 06:01:35 CST 2017

10-11 20:46:23.514 1401 1455 E DropBoxManagerService: Can't write: system_server_crash
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: java.io.FileNotFoundException: /data/system/dropbox/drop19.tmp (Too many open files)
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: at java.io.FileOutputStream.open(Native Method)
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: at java.io.FileOutputStream.<init>(FileOutputStream.java:221)
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: at java.io.FileOutputStream.<init>(FileOutputStream.java:169)
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: at com.android.server.DropBoxManagerService.add(DropBoxManagerService.java:250)
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: at com.android.server.DropBoxManagerService$2.add(DropBoxManagerService.java:129)
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: at android.os.DropBoxManager.addText(DropBoxManager.java:282)
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: at com.android.server.am.ActivityManagerService$24.run(ActivityManagerService.java:14245)
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: at com.android.server.am.ActivityManagerService.addErrorToDropBox(ActivityManagerService.java:14252)
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: at com.android.server.am.ActivityManagerService.handleApplicationCrashInner(ActivityManagerService.java:13813)
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: at com.android.server.am.ActivityManagerService.handleApplicationCrash(ActivityManagerService.java:13797)
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: at com.android.internal.os.RuntimeInit$UncaughtHandler.uncaughtException(RuntimeInit.java:176)
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1068)
10-11 20:46:23.514 1401 1455 E DropBoxManagerService: at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1063)
10-11 20:46:23.614 1401 1473 W WindowManager: Failed looking up window
10-11 20:46:23.614 1401 1473 W WindowManager: java.lang.IllegalArgumentException: Requested window android.view.ViewRootImpl$W@e2ab8e8 does not exist
10-11 20:46:23.614 1401 1473 W WindowManager: at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:9300)
10-11 20:46:23.614 1401 1473 W WindowManager: at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:9291)
10-11 20:46:23.614 1401 1473 W WindowManager: at com.android.server.wm.WindowManagerService.removeWindow(WindowManagerService.java:2411)
10-11 20:46:23.614 1401 1473 W WindowManager: at com.android.server.wm.Session.remove(Session.java:193)
10-11 20:46:23.614 1401 1473 W WindowManager: at android.view.ViewRootImpl.dispatchDetachedFromWindow(ViewRootImpl.java:3324)
10-11 20:46:23.614 1401 1473 W WindowManager: at android.view.ViewRootImpl.doDie(ViewRootImpl.java:5938)
10-11 20:46:23.614 1401 1473 W WindowManager: at android.view.ViewRootImpl.die(ViewRootImpl.java:5915)
10-11 20:46:23.614 1401 1473 W WindowManager: at android.view.WindowManagerGlobal.removeViewLocked(WindowManagerGlobal.java:452)
10-11 20:46:23.614 1401 1473 W WindowManager: at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:390)
10-11 20:46:23.614 1401 1473 W WindowManager: at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:126)
10-11 20:46:23.614 1401 1473 W WindowManager: at com.android.server.policy.PhoneWindowManager.addStartingWindow(PhoneWindowManager.java:2735)
10-11 20:46:23.614 1401 1473 W WindowManager: at com.android.server.wm.WindowManagerService$H.handleMessage(WindowManagerService.java:8290)
10-11 20:46:23.614 1401 1473 W WindowManager: at android.os.Handler.dispatchMessage(Handler.java:102)
10-11 20:46:23.614 1401 1473 W WindowManager: at android.os.Looper.loop(Looper.java:160)
10-11 20:46:23.614 1401 1473 W WindowManager: at android.os.HandlerThread.run(HandlerThread.java:61)
10-11 20:46:23.614 1401 1473 W WindowManager: at com.android.server.ServiceThread.run(ServiceThread.java:46)
10-11 20:46:23.615 1401 1455 W ActivityManager: Force-killing crashed app null at watcher's request

看到Log中有Too many open files,这就是FD泄露,默认每一个进程最多能够打开的文件数量为1024, 一旦达到预置,再试图打开一个文件时就会出错, 即Too many open files。如果看到这个异常,我们可以用lsof命令,对这个进程打开的所有文件进行监控:比如查看头条的进程打开的文件数量,这样下次发生的时候,就知道FD泄露的具体位置是在哪里了。

jason:/ # ps -ef |grep com.ss.android.article.news
u0_a159       7005  1244 5 11:02:44 ?     00:00:44 com.ss.android.article.news
u0_a159       7093  1244 0 11:02:45 ?     00:00:00 com.ss.android.article.news:ad
u0_a159       7303  1244 0 11:02:50 ?     00:00:03 com.ss.android.article.news:push
u0_a159       7384  1244 0 11:02:51 ?     00:00:01 com.ss.android.article.news:pushservice
root          8081  6996 11 11:18:36 pts/0 00:00:00 grep com.ss.android.article.news
jason:/ # 
jason:/ # 
jason:/ # 
jason:/ # lsof -p 7005                                                                                                                                                                                     
COMMAND     PID       USER   FD      TYPE             DEVICE  SIZE/OFF       NODE NAME
id.articl  7005    u0_a159  cwd       DIR                0,1         0          1 /
id.articl  7005    u0_a159  rtd       DIR                0,1         0          1 /
id.articl  7005    u0_a159  txt       REG             259,37     29116        781 /system/bin/app_process32
id.articl  7005    u0_a159  mem       REG             259,37     29116        781 /system/bin/app_process32
id.articl  7005    u0_a159  mem   unknown                                         /dev/ashmem/dalvik-main space (region space) (deleted)
id.articl  7005    u0_a159  mem       REG              252,0     90112    1851444 /data/dalvik-cache/arm/system@framework@boot.art
id.articl  7005    u0_a159  mem       REG              252,0     12288    1851445 /data/dalvik-cache/arm/system@framework@boot-QPerformance.art
id.articl  7005    u0_a159  mem       REG              252,0   2543616    1851446 /data/dalvik-cache/arm/system@framework@boot-core-oj.art
id.articl  7005    u0_a159  mem       REG              252,0   1191936    1851447 /data/dalvik-cache/arm/system@framework@boot-core-libart.art
id.articl  7005    u0_a159  mem       REG              252,0    315392    1851448 /data/dalvik-cache/arm/system@framework@boot-conscrypt.art
id.articl  7005    u0_a159  mem       REG              252,0    192512    1851449 /data/dalvik-cache/arm/system@framework@boot-okhttp.art
id.articl  7005    u0_a159  mem       REG              252,0    413696    1851450 /data/dalvik-cache/arm/system@framework@boot-bouncycastle.art
id.articl  7005    u0_a159  mem       REG              252,0    446464    1851451 /data/dalvik-cache/arm/system@framework@boot-apache-xml.art
id.articl  7005    u0_a159  mem       REG              252,0     20480    1851452 /data/dalvik-cache/arm/system@framework@boot-legacy-test.art
id.articl  7005    u0_a159  mem       REG              252,0    385024    1851453 /data/dalvik-cache/arm/system@framework@boot-ext.art
id.articl  7005    u0_a159  mem       REG              252,0   9338880    1851454 /data/dalvik-cache/arm/system@framework@boot-framework.art
id.articl  7005    u0_a159  mem       REG              252,0    831488    1851455 /data/dalvik-cache/arm/system@framework@boot-telephony-common.art
id.articl  7005    u0_a159  mem       REG              252,0     53248    1851456 /data/dalvik-cache/arm/system@framework@boot-voip-common.art
id.articl  7005    u0_a159  mem       REG              252,0     57344    1851457 /data/dalvik-cache/arm/system@framework@boot-ims-common.art
id.articl  7005    u0_a159  mem       REG              252,0    204800    1851458 /data/dalvik-cache/arm/system@framework@boot-org.apache.http.legacy.boot.art
id.articl  7005    u0_a159  mem       REG              252,0      8192    1851459 /data/dalvik-cache/arm/system@framework@boot-android.hidl.base-V1.0-java.art
id.articl  7005    u0_a159  mem       REG              252,0     16384    1851460 /data/dalvik-cache/arm/system@framework@boot-android.hidl.manager-V1.0-java.art
id.articl  7005    u0_a159  mem       REG              252,0      8192    1851461 /data/dalvik-cache/arm/system@framework@boot-tcmiface.art
id.articl  7005    u0_a159  mem       REG              252,0     12288    1851462 /data/dalvik-cache/arm/system@framework@boot-telephony-ext.art
id.articl  7005    u0_a159  mem       REG              252,0     36864    1851463 /data/dalvik-cache/arm/system@framework@boot-WfdCommon.art
id.articl  7005    u0_a159  mem       REG              252,0     12288    1851464 /data/dalvik-cache/arm/system@framework@boot-oem-services.art
id.articl  7005    u0_a159  mem       REG              252,0     45056    1851465 /data/dalvik-cache/arm/system@framework@boot-qcom.fmradio.art
id.articl  7005    u0_a159  mem       REG              252,0    516096    1851466 /data/dalvik-cache/arm/system@framework@boot-miui.art
id.articl  7005    u0_a159  mem       REG              252,0    290816    1851467 /data/dalvik-cache/arm/system@framework@boot-miuisystem.art
id.articl  7005    u0_a159  mem       REG             259,37    212088       1978 /system/framework/arm/boot.oat
id.articl  7005    u0_a159  mem       REG             259,37     26020       1909 /system/framework/arm/boot-QPerformance.oat
id.articl  7005    u0_a159  mem       REG             259,37  12007484       1933 /system/framework/arm/boot-core-oj.oat
.....

含义:

COMMAND:进程的名称
PID:进程标识符
USER:进程所有者
FD:文件描述符,应用程序通过文件描述符识别该文件。如cwd、txt等
TYPE:文件类型,如DIR、REG等
DEVICE:指定磁盘的名称
SIZE:文件的大小
NODE:索引节点(文件在磁盘上的标识)
NAME:打开文件的确切名称

除了打开文件会申请fd之外,每打开一个socket都会增加一个fd,每次创建一个线程也会打开一个fd。系统中经常会有fd泄露的问题存在,所以O上发生NE时会将fd信息打印到tombstone文件中。对于JAVA的,只有自己想办法监控了。下面模拟一下FD泄露。

有些厂商会自己修改Max open files的值,比如华为,在小米手机上仍然是1024,我们可以在prop/pid/limits中查看。

2|jason:/proc/32194 # cat limits                                                                                                                                                                           
Limit                     Soft Limit           Hard Limit           Units     
Max cpu time              unlimited            unlimited            seconds   
Max file size             unlimited            unlimited            bytes     
Max data size             unlimited            unlimited            bytes     
Max stack size            8388608              unlimited            bytes     
Max core file size        0                    unlimited            bytes     
Max resident set          unlimited            unlimited            bytes     
Max processes             22019                22019                processes 
Max open files            1024                 4096                 files     
Max locked memory         67108864             67108864             bytes     
Max address space         unlimited            unlimited            bytes     
Max file locks            unlimited            unlimited            locks     
Max pending signals       22019                22019                signals   
Max msgqueue size         819200               819200               bytes     
Max nice priority         40                   40                   
Max realtime priority     0                    0                    
Max realtime timeout      unlimited            unlimited            us        

现在可以模拟一个fd泄露的问题,代码如下:

public class MainActivity extends Activity {
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       View view = findViewById(R.id.open_thread);
       view.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View view) {
               for(int i=0;i<1024;i++){
                   HandlerThread   mWorkHandler = new HandlerThread("workHandlerThread");
                   mWorkHandler.start();
               }
           }
       });
   }
}

运行之后,几秒就crash了,抓一份284Log(或者去data/tombstone中),看一下trace。

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Xiaomi/jason/jason:8.1.0/OPM1.171019.019/8.6.20:user/release-keys'
Revision: '0'
ABI: 'arm64'
pid: 20303, tid: 20800, name: workHandlerThre  >>> com.example.wangjing.rebootdemo <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: 'Could not make wake event fd: Too many open files'
   x0   0000000000000000  x1   0000000000005140  x2   0000000000000006  x3   0000000000000008
   x4   0000000000000000  x5   0000000000000000  x6   0000000000000000  x7   7f7f7f7f7f7f7f7f
   x8   0000000000000083  x9   3dbbf71453109433  x10  0000000000000000  x11  0000000000000001
   x12  ffffffffffffffff  x13  ffffffffffffffff  x14  ff00000000000000  x15  ffffffffffffffff
   x16  000000025c938fa8  x17  000000719e6915c4  x18  0000000000000008  x19  0000000000004f4f
   x20  0000000000005140  x21  00000070ea2e6800  x22  00000070e5166588  x23  000000711ae3214a
   x24  0000000000000000  x25  00000070e5166588  x26  00000070ea2e68a0  x27  0000000000000000
   x28  0000000000000000  x29  00000070e5164b70  x30  000000719e646e38
   sp   00000070e5164b30  pc   000000719e646e54  pstate 0000000060000000
   v0   2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e  v1   2e6761742e676f6c2e74736973726570
   v2   00000070e516658800007265706f6f4c  v3   00000000000000000000000000000000
   v4   80200800000000008020000000000000  v5   40000000400000004000000000000000
   v6   00000000000000000000000000000000  v7   80200802802008028020080280200802
   v8   00000000000000000000000000000000  v9   00000000000000000000000000000000
   v10  00000000000000000000000000000000  v11  00000000000000000000000000000000
   v12  00000000000000000000000000000000  v13  00000000000000000000000000000000
   v14  00000000000000000000000000000000  v15  00000000000000000000000000000000
   v16  40100401401004014010040140100401  v17  a0080000a0000000a800a00040404000
   v18  80200800000000008020000000000000  v19  00000000000000000000000000000000
   v20  00000000000000000000000000000000  v21  00000000000000000000000000000000
   v22  00000000000000000000000000000000  v23  000000000000000000000000433b0000
   v24  0000000000000000000000003f800000  v25  0000000000000000000000003f800000
   v26  000000000000000000000071a011ebcc  v27  000000000000000000000071a011ec24
   v28  000000000000000000000071a011ef64  v29  000000000000000000000071a011e5bc
   v30  000000000000000000000071a011e7b4  v31  000000000000000000000071a011e8e8
   fpsr 00000013  fpcr 00000000

backtrace:
   #00 pc 000000000001de54  /system/lib64/libc.so (abort+104)
   #01 pc 0000000000007f20  /system/lib64/liblog.so (__android_log_assert+304)
   #02 pc 00000000000154d0  /system/lib64/libutils.so (android::Looper::Looper(bool)+296)
   #03 pc 00000000001114b8  /system/lib64/libandroid_runtime.so (android::NativeMessageQueue::NativeMessageQueue()+160)
   #04 pc 0000000000111de8  /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativeInit(_JNIEnv*, _jclass*)+28)
   #05 pc 00000000009c88e0  /system/framework/arm64/boot-framework.oat (offset 0x9c6000) (android.os.Binder.clearCallingIdentity [DEDUPED]+144)
   #06 pc 000000000054904c  /system/lib64/libart.so (art_quick_invoke_static_stub+604)
   #07 pc 00000000000dcfb4  /system/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+260)
   #08 pc 000000000029a950  /system/lib64/libart.so (art::interpreter::ArtInterpreterToCompiledCodeBridge(art::Thread*, art::ArtMethod*, art::ShadowFrame*, unsigned short, art::JValue*)+344)
   #09 pc 0000000000294f40  /system/lib64/libart.so (_ZN3art11interpreter6DoCallILb0ELb0EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE+696)
   #10 pc 0000000000532258  /system/lib64/libart.so (MterpInvokeStatic+224)
   #11 pc 000000000053ab94  /system/lib64/libart.so (ExecuteMterpImpl+14612)
   #12 pc 00000000002753a4  /system/lib64/libart.so (art::interpreter::Execute(art::Thread*, art::DexFile::CodeItem const*, art::ShadowFrame&, art::JValue, bool)+444)
   #13 pc 000000000027afac  /system/lib64/libart.so (art::interpreter::ArtInterpreterToInterpreterBridge(art::Thread*, art::DexFile::CodeItem const*, art::ShadowFrame*, art::JValue*)+216)
   #14 pc 0000000000294f20  /system/lib64/libart.so (_ZN3art11interpreter6DoCallILb0ELb0EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE+664)
   #15 pc 000000000053209c  /system/lib64/libart.so (MterpInvokeDirect+304)
   #16 pc 000000000053ab14  /system/lib64/libart.so (ExecuteMterpImpl+14484)
   #17 pc 00000000002753a4  /system/lib64/libart.so (art::interpreter::Execute(art::Thread*, art::DexFile::CodeItem const*, art::ShadowFrame&, art::JValue, bool)+444)
   #18 pc 000000000027afac  /system/lib64/libart.so (art::interpreter::ArtInterpreterToInterpreterBridge(art::Thread*, art::DexFile::CodeItem const*, art::ShadowFrame*, art::JValue*)+216)
   #19 pc 0000000000294f20  /system/lib64/libart.so (_ZN3art11interpreter6DoCallILb0ELb0EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE+664)
   #20 pc 000000000053209c  /system/lib64/libart.so (MterpInvokeDirect+304)
   #21 pc 000000000053ab14  /system/lib64/libart.so (ExecuteMterpImpl+14484)
   #22 pc 00000000002753a4  /system/lib64/libart.so (art::interpreter::Execute(art::Thread*, art::DexFile::CodeItem const*, art::ShadowFrame&, art::JValue, bool)+444)
   #23 pc 000000000027afac  /system/lib64/libart.so (art::interpreter::ArtInterpreterToInterpreterBridge(art::Thread*, art::DexFile::CodeItem const*, art::ShadowFrame*, art::JValue*)+216)
   #24 pc 0000000000294f20  /system/lib64/libart.so (_ZN3art11interpreter6DoCallILb0ELb0EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE+664)
   #25 pc 0000000000532258  /system/lib64/libart.so (MterpInvokeStatic+224)
   #26 pc 000000000053ab94  /system/lib64/libart.so (ExecuteMterpImpl+14612)
   #27 pc 00000000002753a4  /system/lib64/libart.so (art::interpreter::Execute(art::Thread*, art::DexFile::CodeItem const*, art::ShadowFrame&, art::JValue, bool)+444)
   #28 pc 0000000000523ab4  /system/lib64/libart.so (artQuickToInterpreterBridge+1052)
   #29 pc 0000000000551f0c  /system/lib64/libart.so (art_quick_to_interpreter_bridge+92)
   #30 pc 000000000000a080  /dev/ashmem/dalvik-jit-code-cache (deleted)

果然出现了Could not make wake event fd: Too many open files。

pid: 20303, tid: 20800, name: workHandlerThre  >>> com.example.wangjing.rebootdemo <<<

现在就需要去代码中查找workHandlerThread是什么了。

三、如何创建一个进程

在linux中可以使用fork()来创建一个进程,来看下函数的定义以及返回值,函数原型 pid_t fork(void)
函数返回值: 0: 子进程 , -1: 出错, >0: 父进程

#include <unistd.h>
#include <stdio.h>
#include <wait.h>
int main() {
   int count = 0;
   pid_t fpid = fork();
   if (fpid < 0) {
       printf("创建父子进程失败!");
   } else if (fpid == 0) {
       printf("子进程ID:%d\n", getpid());
       count++;
   } else {
       printf("父进程ID:%d\n", getpid());
       count=10;
   }
   printf("count=%d\n", count);
   waitpid(fpid, NULL, 0);
   return 0;
}
/home/wangjing/CLionProjects/untitled/cmake-build-debug/untitled
父进程ID:15229
count=10
子进程ID:15230
count=1

Process finished with exit code 0

通过打印的结果有两点重要信息需要get。

  • 1、fork函数执行一次,返回两次,第一次返回父进程的id,第二次返回子进程的id。
  • 2、count是全局变量,子进程和父进程同时操作,但是互相不受影响

利用fork()函数将整个程序分成了两半,在pid_t fpid==0是子进程执行的分支,大于0则是父进程执行的分支。 count=0这个变量被原封不动地拷贝到这两个分支之中。


fork原理.png

一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。 其实进程的fork基于写时复制技术,相对与传统fork技术更加高效。何为写时复制技术呢?

内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。

假设现在有一个进程p1,包括正文段(可重入的程序,能被若干进程共享,比如代码等),数据段(用于保存程序已经初始化的变量),堆,栈。也有文件描述符等。


传统的fork技术.png

可以看到传统的fork系统调用直接把父进程所有的资源复制给新创建的进程,如果这时子进程执行exec函数系统调用,那么这种复制毫无意义,在看写时复制技术。

写时复制技术.png

fork()之后父进程的将自己的虚拟空间拷贝给子进程,使得子进程可以共享父进程的物理空间,节省了很多物理内存。等到子进程需要写的时候,内核会为子进程分配数据段,堆,栈等,而正文段段继续共享父进程的。很显然,基于写时复制,进程的创建会更加高效。

四、进程信号

进程间的通信除了上层所说的socket,binder,管道等等,还可以用信号来交流,信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。系统支持的所有信号如下所示:

4.1信号种类

Android系统中的信号我们可以用 adb shell kill -l 查看

wangjing@wangjing-OptiPlex-7050:~$ adb shell kill -l
 1    HUP Hangup                        33     33 Signal 33
 2    INT Interrupt                     34     34 Signal 34
 3   QUIT Quit                          35     35 Signal 35
 4    ILL Illegal instruction           36     36 Signal 36
 5   TRAP Trap                          37     37 Signal 37
 6   ABRT Aborted                       38     38 Signal 38
 7    BUS Bus error                     39     39 Signal 39
 8    FPE Floating point exception      40     40 Signal 40
 9   KILL Killed                        41     41 Signal 41
10   USR1 User signal 1                 42     42 Signal 42
11   SEGV Segmentation fault            43     43 Signal 43
12   USR2 User signal 2                 44     44 Signal 44
13   PIPE Broken pipe                   45     45 Signal 45
14   ALRM Alarm clock                   46     46 Signal 46
15   TERM Terminated                    47     47 Signal 47
16 STKFLT Stack fault                   48     48 Signal 48
17   CHLD Child exited                  49     49 Signal 49
18   CONT Continue                      50     50 Signal 50
19   STOP Stopped (signal)              51     51 Signal 51
20   TSTP Stopped                       52     52 Signal 52
21   TTIN Stopped (tty input)           53     53 Signal 53
22   TTOU Stopped (tty output)          54     54 Signal 54
23    URG Urgent I/O condition          55     55 Signal 55
24   XCPU CPU time limit exceeded       56     56 Signal 56
25   XFSZ File size limit exceeded      57     57 Signal 57
26 VTALRM Virtual timer expired         58     58 Signal 58
27   PROF Profiling timer expired       59     59 Signal 59
28  WINCH Window size changed           60     60 Signal 60
29     IO I/O possible                  61     61 Signal 61
30    PWR Power failure                 62     62 Signal 62
31    SYS Bad system call               63     63 Signal 63
32     32 Signal 32                     64     64 Signal 64

4.1信号的产生

我们可以使用$ adb shell kill -{signum} {pid}给对应的进程发送信号,如果遇到系统卡死,需要抓取system_server的trace,就可以使用kill -3。

 adb shell kill  -3  systemserver_pid

生成的system_server trace在data/anr/traces.txt中。
在APP发送Native Crash或者系统发送Native Crash的时候,会发送对应的信号,一个有经验的程序员就会知道这种信号的意思。

写一个系统发送信号的小例子

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

int main(){
    char *str = "signal";
    *str = 'a'; 
    printf("%s\n", str);
    return 0;
}

gcc编译成可运行的程序

gcc signal.c -g -o signalApp 

开始运行

wangjing@wangjing-OptiPlex-7050:~/桌面$ ./signalApp 
段错误 (核心已转储)

发现错误,开始调试

wangjing@wangjing-OptiPlex-7050:~/桌面$ gdb signalApp 
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from signalApp...done.
(gdb) r
Starting program: /home/wangjing/桌面/signalApp 

Program received signal SIGSEGV, Segmentation fault.
0x000000000040053a in main () at signal.c:6
6       *str = 'a'; 
(gdb) 

可以看到信号是SIGSEGV(11),这是一个段错误,11我们最常见的信号,分SEGV_MAPERR和SEGV_ACCERR两种。第一种是SEGV_MAPERR,意为地址不在进程地址空间内时触发,比如:

pid: 1219, tid: 1219, name: ndroid.systemui >>> com.android.systemui <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000
r0 00000000 r1 00000000 r2 000010a0 r3 00000175
r4 be9bc150 r5 00000000 r6 be9bbfac r7 4053763d
r8 00000174 r9 00001200 sl 00001200 fp 000010e0
ip 40664b9c sp be9bbf8c lr 40538701 pc 40128310 cpsr 200f0010
backtrace:
#00 pc 00022310 /system/lib/libc.so (memset+24)
#01 pc 000b66ff /system/lib/libskia.so (SkDraw::drawPaint(SkPaint const&) const+286)
#02 pc 000b1023 /system/lib/libskia.so (SkCanvas::internalDrawPaint(SkPaint const&)+66)
#03 pc 000aff65 /system/lib/libskia.so (SkCanvas::drawColor(unsigned int, SkXfermode::Mode)+44)
#04 pc 0002034c /system/lib/libdvm.so (dvmPlatformInvoke+112)

从调用栈中可以看出,程序执行到memset+24的位置时,cpu发现异常。我们可以通过gdb或者objdump工具查看这个汇编:

(gdb) disassemble 0x401282f8
Dump of assembler code for function memset:
   0x401282f8 <+0>:    stmfd    sp!, {r0}
   0x401282fc <+4>:    vdup.8    q0, r1
   0x40128300 <+8>:    subs    r2, r2, #32
   0x40128304 <+12>:    bcc    0x4012e318 <memset+32>
   0x40128308 <+16>:    vorr    q1, q0, q0
   0x4012830c <+20>:    subs    r2, r2, #32
   0x40128310 <+24>:    vst1.8    {d0-d3}, [r0]!            <<<<

可以看到是把d0-d3寄存器的值写到r0寄存器指向的地址时发生的异常。我们可以从r0寄存器的值可以知道,这个地址是0x00000000,而0x00000000不在进程地址空间范围内,所以会引起SEGV_MAPERR错误。"fault addr 00000000" 这个信息也能说明问题,但我们不看汇编不能确定是哪个寄存器(r0和r1都有可能)。看了汇编后能确定是r0,也就是memset的第一个参数为空导致了这个问题。

第二种为SEGV_ACCERR,意为地址在进程地址空间内,但访问权限不够时触发,比如


Build fingerprint: 'Xiaomi/virgo/virgo:6.0.1/MMB29M/7.1.19-internal:user/test-keys'
Revision: '0'
ABI: 'arm'
pid: 26620, tid: 26867, name: DetectorThread  >>> com.linecorp.b612.android <<<
signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x9577b090
    r0 00000034  r1 9591b000  r2 00001000  r3 12ced228
    r4 b4a7f378  r5 000010e9  r6 b46d73cb  r7 000010e9
    r8 9577ae70  r9 00000004  sl 00000004  fp b6cc3050
    ip 00001000  sp 9577ae10  lr b6c8acf5  pc 9577b090  cpsr 200f0010
 
backtrace:
    #00 pc 00102090  [stack:26620]
    #01 pc 00047cf1  /system/lib/libc.so (__sread+16)
    #02 pc 000025b4  /system/lib/libart.so (offset 0x44d000)

fault addr 0x9577b090, SEGV_ACCERR错误指的是访问权限的问题,如写只读段,或者是执行数据段的内容.寄存器的值中发现PC是pc 9577b090,那么有可能是地址9577b090所处的段的属性有问题.查看tombstone中的maps信息:

    95678000-95678fff ---         0      1000
--->95679000-9577bfff rw-         0    103000  [stack:26620]
    9577c000-9578cfff rw-     a9000     11000  /dev/kgsl-3d0

0x9577b090处在区间[95679000,9577bfff]内,这段地址是有rw权限的,少的是x权限,也就是可执行权限。所以抛出了SEGV_ACCERR的错误,这种一般是函数指针被某一个家伙覆盖导致的。进程信号有很多种,不一一赘述,掌握这些信号的常见场景,需要一定经验和时间。

Relevant Link:
linux内核数据结构学习总结
The Linux Process Principle,NameSpace, PID、TID、PGID、PPID、SID、TID、TTY
https://www.cnblogs.com/wuchanming/p/4495479.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 又来到了一个老生常谈的问题,应用层软件开发的程序员要不要了解和深入学习操作系统呢? 今天就这个问题开始,来谈谈操...
    tangsl阅读 4,004评论 0 23
  • 本文转载自实验楼:多进程(一) 概述 进程的概念这里就不再过多的赘述了,市面上几乎关于计算机操作系统的书都有详细的...
    mnikn阅读 470评论 0 0
  • Linux 进程管理与程序开发 进程是Linux事务管理的基本单元,所有的进程均拥有自己独立的处理环境和系统资源,...
    JamesPeng阅读 2,405评论 1 14
  • 我们开展了最佳用户体验演练的系列活动,其涵盖了模式和格式塔理论是如何帮助我们设计便于用户理解的界面。如今,我们要涉...
    huaer阅读 279评论 0 0
  • 忆颗石头树 推荐一首歌:Spring Tracy Chapman - Our Bright Future !Q_Q...
    忆颗石头树阅读 317评论 0 2