v1.0 1st Edition

“proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。用户和应用程序可以通过proc得到系统的信息,并可以改变内核的某些参数。”

“/proc 文件系统是一种内核和内核模块用来向进程 (process) 发送信息的机制 (所以叫做 /proc)。这个伪文件系统让你可以和内核内部数据结构进行交互,获取 有关进程的有用信息,在运行中 (on the fly) 改变设置 (通过改变内核参数)。 与其他文件系统不同,/proc 存在于内存之中而不是硬盘上

/proc 由内核控制,没有承载 /proc 的设备。因为 /proc 主要存放由内核控制的状态信息,所以大部分这些信息的逻辑位置位于内核控制的内存。对 /proc 进行一次 ls -l 可以看到大部分文件都是 0 字节大的;不过察看这些文件的时候,确实可以看到一些信息。这怎么可能?这是因为 /proc 文件系统和其他常规的文件系统一样把自己注册到虚拟文件系统层 (VFS) 了。然而,直到当 VFS 调用它,请求文件、目录的 i-node 的时候,/proc 文件系统才根据内核中的信息建立相应的文件和目录。”

本文在Android的范畴内讨论/proc文件系统,由于存在一定的权限问题,过深的向Linux靠近也意义不大。而正因为是存在于内存的动态文件系统,所以以Java API进行操作可能会出现一些问题。比如使用File对象会显示文件不存在,目录判断成文件等问题,当然有可能是因为临时产生的进程的原因。而在Java 代码中调用的命令行功能也比较弱,在具体使用中会非常的不便。

/proc中大致可获得两类信息:系统信息和应用信息。

在/proc目录下的数字目录都可以理解为进程信息所在目录,而文件夹所表示的数字即为进程ID(PID),其余为系统信息文件和目录。同时进程目录中的文件结构几乎和外层系统目录结构一致,所以基本上对于基本信息的读取和处理是相同的。

对于系统信息的读取比较常用的有一下几点:

系统性能相关参数:

分别用到/proc/stat、/proc/net/dev、/proc/meminfo,如下图。

内存使用率:

在/proc/stat文件中获取四组数据:用户模式(user)、低优先级的用户模式(nice)、内核模式(system)和空闲处理器时间(idle)。

CPU列的含义分别为:

user (131706) 从系统启动开始累计到当前时刻,用户态的CPU时间(单位:jiffies) ,不包含 nice值为负进程。1jiffies=0.01秒
nice (21541) 从系统启动开始累计到当前时刻,nice值为负的进程所占用的CPU时间(单位:jiffies)
system (98190) 从系统启动开始累计到当前时刻,核心时间(单位:jiffies)
idle (2629405) 从系统启动开始累计到当前时刻,除硬盘IO等待时间以外其它等待时间(单位:jiffies)
iowait (14924) 从系统启动开始累计到当前时刻,硬盘IO等待时间(单位:jiffies)
irq (21) 从系统启动开始累计到当前时刻,硬中断时间(单位:jiffies)
softirq (3039) 从系统启动开始累计到当前时刻,软中断时间(单位:jiffies)

CPU时间=user + nice + system + idle + iowait + irq + softirq

“intr”这行给出中断的信息,第一个为自系统启动以来,发生的所有的中断的次数;然后每个数对应一个特定的中断自系统启动以来所发生的次数。
“ctxt”给出了自系统启动以来CPU发生的上下文交换的次数。
“btime”给出了从系统启动到现在为止的时间,单位为秒。
“processes (total_forks) 自系统启动以来所创建的任务的个数目。
“procs_running”:当前运行队列的任务的数目。
“procs_blocked”:当前被阻塞的任务的数目。

cpu usage:

cpu_time = user + nice + system + idle + iowait + irq + softirq
cpu_work = user + nice + system + idle

cpu_usage = (cpu_work_2 - cpu_work_1) / (cpu_time2 - cpu_time1) ) * 100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CmdResolver resolver = CmdActuator.cat("/proc/stat");
String detail = resolver.parseDetail();
String[] args = detail.split("\\n")[0].split("\\s+");
long workTimeStart = StatUtil.formatWorkTime(args);
long totalTimeStart = StatUtil.formatTotalTime(workTimeStart, args);
try {
Thread.sleep(300);
}catch (Exception ignored){}
resolver = CmdActuator.cat("/proc/stat");
detail = resolver.parseDetail();
args = detail.split("\\n")[0].split("\\s+");
long workTimeEnd = StatUtil.formatWorkTime(args);
long totalTimeEnd = StatUtil.formatTotalTime(workTimeEnd, args);

long totalTime = totalTimeEnd - totalTimeStart;
double workTime = workTimeEnd - workTimeStart;
double num = workTime * 100 / totalTime;
String cpuUsage = String.format(Locale.getDefault(), "%.2f %%", num);

代码如上,详细代码和工具方法参见github代码SystemPerformanceTask.java。或者使用更加方便的方式,top获得基本的信息。

1
2
top -n 1
top -n 1 | grep com.xx.xx

内存使用率

1
2
3
4
5
6
7
8
9
10
11
CmdResolver memResolver = CmdActuator.cat("/proc/meminfo");
String[] items = memResolver.parseDetail().split("\\n");
long memory = Long.parseLong(items[0].split("\\s+")[1]) * 1024;
String totalMemory = Formatter.formatFileSize(context, memory)

...

ActivityManager manager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
manager.getMemoryInfo(memoryInfo);
String freeMemory = Formatter.formatFileSize(context, memoryInfo.availMem)

网络:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CmdResolver netResolver = CmdActuator.cat("/proc/net/dev");
String detail = netResolver.parseDetail();
//android.net.TrafficStats
long netSend = 0;
long netReceive = 0;
String[] lines = detail.split("\\n");
for (String line : lines){
if (TextUtils.isEmpty(line)) continue;
if (!line.contains("wlan0")) continue;
String[] items = line.trim().split("\\s+");
netReceive = Long.parseLong(items[1]);
netSend = Long.parseLong(items[9]);
break;
}
String receiveSize = Formatter.formatFileSize(context, netReceive);
String sendSize = Formatter.formatFileSize(context, netSend);

android.net.TrafficStats获取网络使用情况会更加方便,并且有更加详细的分类.

mounts信息

mounts信息非常不容易阅读,并且包含一些文件系统和挂载点的信息。如果需要这些信息可以解析,但是如果仅仅是读取分区的话使用storage API会更加方便简洁,并且对于分区大小和媒体资源统计也有非常方便的接口。另外由于Storage API实际上很多是私有API,所以如果想要更加方便的使用建议在framework源码环境下直接使用,如果使用反射反而更加繁琐了。

其它

/proc文件系统中也可以获得许多其它有用的信息,如上图CPU信息。经过测试发现在普通应用权限下,有些文件无法获得内容或者有权限问题无法读取。直接在adb shell中几乎都可以通过cat命令获取文件内容,另外还专门对比了一下linux和android下的proc目录的结构和几乎所有文件内容。

应用信息

/proc/pid,目录下为每个进程的信息,其文件和目录结构与外层proc非常相似。

线程与进程调度
这里涉及一些进程个线程调度的问题,由于这里的信息为只读状态,所以过多的说明也不是很有意义。关于进程和线程调度以及在系统层和framework层的相关实现,找机会进行专门的说明。

cgroups

其名称源自控制组群(control groups)的简写,是Linux内核的一个功能,用来限制,控制与分离一个进程组群的资源(如CPU、内存、磁盘输入输出等)。

在Android中也存在cgroups,涉及到CPU的目前只有两个,一个是apps,路径为 /dev/cpuctl/apps 。另一个是bg_non_interactive,路径为 /dev/cpuctl/apps/bg_non_interactive

在Android中cgroup反映两种状态。即apps(前台进程),bg_non_interactive(后台进程)。首先判断当前系统是否支持cgroup,条件是是否存在文件/dev/cpuctl/tasks

如上图,系统android 6.1,分别为一个前台进程和一个后台进程,可以看出在cpu行中后台进程有bg_non_interactive标识,而pucacct行中份有此进程的uid和pid,这里的pid和uid在使用ps命令时第一列值即为UID,其值是前缀_(uid-10000),第二列为pid(即进程ID),所以我们可以根据ps的值快速定位到proc文件系统,避免对proc中文件的直接遍历。毕竟/proc/pid在实际设备中数量比较大,遍历起来非常耗时,并且很大一部分是后台服务,在ps中根据进程名可以排除大部分不需要遍历判断的进程。

另外在android 5.0以下系统其结构稍有不同。

进程stat

如果不支持cgroup的系统,则使用stat中的值进行判断,即/proc/pid/stat文件中第17列的值。如果等于0则此进程为前台进程。

使用范围

根据Google Plus(Android Framework成员)上的说明,Linux scheduler无法提供充足的线程策略信息,于是从Android 1.6开始将此功能转移至cgroup。因此至少在Android 7之前此方法是完全没有问题的。

如上图Android cgroup只有两个组分别为apps和bg_non_interactive。cpuctl目录下为apps(前台)组信息,而bg_non_interactive(后台)目录下结构和cpuctl一致,可以使用同样的方式读取。最关键的是在android 6及以下这些目录是完全由读取权限的。

这其中比如cpu.shares文件记录cgroup分组中任务获得CPU时间的相对值,前台和后台的分别为1024和52。而cpu.rt_period_uscpu.rt_runtime_us分别设置对应cgroup获得CPU资源的周期和人物可以最长获得CPU资源的时间。

线程与进程调度

看了这些之后发现对线程和进程调度更加感兴趣了,proc虚拟文件系统仅仅是对于内存信息的读取所以仅仅从这里谈线程与进程调度意义不大。实际中线程与进程调度有linux内核,android framework层几处分工不同的地方。而在使用中又有android API和一些Java API可供使用,同时线程和进程调度在实际开发中也有着比较重要的作用,所以之后使用另起篇幅专门对此问题进行说明。

源代码

为了快速读取/proc信息这里实现了一个简单的信息读取应用,与其说为了读取信息其更主要的目的是测试在普通应用权限下是否可以读取这些虚拟文件。

本项目源码详见GitHub 《Proc》