前提
AOSP タグ:android-10.0.0_r41
kernel/common タグ: ASB-2020-08-05_4.19-q-release
はじめに
Android 8以降、I/Oの統計情報をUID毎に出力するstoragedと呼ばれるデーモンが実装されている。
このデーモンはAndroid公式で説明されているように、Android独自procfsである"/proc/uid_io/stats"から取得できる情報がソースとなっている。
本procfsはどのようにしてI/O統計を実現しているかを、備忘録をかねてソースベースで見ていく。
TL;DR
・task_struct構造体に保持されているI/O統計(/proc/[pid]/io)をUIDと紐づけるためのI/F。
・UID, taskそれぞれのI/O統計情報をhashtableを利用して管理。
・統計情報の更新タイミングは、
・/proc/uid_io/statsのopen時
・/proc/uid_procstat/setへのwriteでforeground/backgroundが変化した場合
・task(process)の終了時(PROFILE_TASK_EXITイベント)
ソースコードリーディング
■proc/uid_io/stats
proc/uid_io/statsをopenすると、以下によりhash_table上の各UIDに対応するエントリが保持している統計情報を呼び出し元に与える。
static int uid_io_show(struct seq_file *m, void *v)
{
struct uid_entry *uid_entry;
unsigned long bkt;
rt_mutex_lock(&uid_lock);
update_io_stats_all_locked();
hash_for_each(hash_table, bkt, uid_entry, hash) {
seq_printf(m, "%d %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu\n",
uid_entry->uid,
uid_entry->io[UID_STATE_FOREGROUND].rchar,
uid_entry->io[UID_STATE_FOREGROUND].wchar,
uid_entry->io[UID_STATE_FOREGROUND].read_bytes,
uid_entry->io[UID_STATE_FOREGROUND].write_bytes,
uid_entry->io[UID_STATE_BACKGROUND].rchar,
uid_entry->io[UID_STATE_BACKGROUND].wchar,
uid_entry->io[UID_STATE_BACKGROUND].read_bytes,
uid_entry->io[UID_STATE_BACKGROUND].write_bytes,
uid_entry->io[UID_STATE_FOREGROUND].fsync,
uid_entry->io[UID_STATE_BACKGROUND].fsync);
show_io_uid_tasks(m, uid_entry);
}
rt_mutex_unlock(&uid_lock);
return 0;
}
参照しているデータ構造は以下。各entry構造体はサイズ5のio_stats構造体を持つ。
実際にprocfsより参照されるのはFOREGROUND/BACKGROUNDの二種であり、その他は内部処理用。
io_stats構造体は、ストレージデバイスへのread/write byte数や、read()/write()システムコールにで要求された文字数を分けて保持する。
struct io_stats {
u64 read_bytes; // read I/O events from a storage device (byte)
u64 write_bytes; // write I/O events from a storage device (byte)
u64 rchar; // read() syscall (byte)
u64 wchar; // write() syscall (byte)
u64 fsync; // fsync
};
#define UID_STATE_FOREGROUND 0 // フォアグラウンド時のstats
#define UID_STATE_BACKGROUND 1 // バックグラウンド時のstats
#define UID_STATE_BUCKET_SIZE 2
#define UID_STATE_TOTAL_CURR 2 // 現在のstats
#define UID_STATE_TOTAL_LAST 3 // 更新前のstats
#define UID_STATE_DEAD_TASKS 4 // UIDに紐づく終了した全taskのstats
#define UID_STATE_SIZE 5
struct uid_entry {
uid_t uid;
u64 utime;
u64 stime;
u64 active_utime;
u64 active_stime;
int state;
struct io_stats io[UID_STATE_SIZE];
struct hlist_node hash;
#ifdef CONFIG_UID_SYS_STATS_DEBUG
DECLARE_HASHTABLE(task_entries, UID_HASH_BITS);
#endif
};
struct task_entry {
char comm[MAX_TASK_COMM_LEN];
pid_t pid;
struct io_stats io[UID_STATE_SIZE];
struct hlist_node hash;
};
また、UIDに紐づく各taskの統計情報も同時に出力。
static void show_io_uid_tasks(struct seq_file *m, struct uid_entry *uid_entry)
{
struct task_entry *task_entry;
unsigned long bkt_task;
hash_for_each(uid_entry->task_entries, bkt_task, task_entry, hash) {
/* Separated by comma because space exists in task comm */
seq_printf(m, "task,%s,%lu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu\n",
task_entry->comm,
(unsigned long)task_entry->pid,
/* ・・・ */
}
}
■hash_tableへのエントリ追加と統計情報の再計算
このhash_tableへのエントリ追加は、find_or_register_uid()を通して行われる。
この関数自体はhashtableからUIDのエントリを探索し、なければ追加する処理。
static struct uid_entry *find_or_register_uid(uid_t uid)
{
struct uid_entry *uid_entry;
uid_entry = find_uid_entry(uid);
if (uid_entry)
return uid_entry;
// uidがhashtableに存在しなければ
uid_entry = kzalloc(sizeof(struct uid_entry), GFP_ATOMIC);
if (!uid_entry)
return NULL;
uid_entry->uid = uid;
#ifdef CONFIG_UID_SYS_STATS_DEBUG
hash_init(uid_entry->task_entries);
#endif
hash_add(hash_table, &uid_entry->hash, uid); // hashtableへ追加。
return uid_entry;
}
find_or_register_uid()は、
- update_io_stats_all_locked()
- uid_procstat_write()
- process_notifier()
- uid_cputime_show()
よりコールされる。それぞれに関して確認する。
1. update_io_stats_all_locked()
これは前述のuid_io_show()のI/O統計情報表示前にコールされる。
この関数の目的は全UID/taskの現在の状態(foreground or background)のio_statsを更新すること。
まず、全task_structが保持する統計情報を、taskが属するuid_entry、および対応するtask_entryのUID_STATE_TOTAL_CURRに加算する。
static void update_io_stats_all_locked(void)
{
struct uid_entry *uid_entry = NULL;
struct task_struct *task, *temp;
struct user_namespace *user_ns = current_user_ns();
unsigned long bkt;
uid_t uid;
// すべてのUID、taskのtotal currentの統計情報を0に。
hash_for_each(hash_table, bkt, uid_entry, hash) {
memset(&uid_entry->io[UID_STATE_TOTAL_CURR], 0,
sizeof(struct io_stats));
set_io_uid_tasks_zero(uid_entry);
}
rcu_read_lock();
do_each_thread(temp, task) { // 全taskに対して
uid = from_kuid_munged(user_ns, task_uid(task)); // taskの属するUIDを取得
if (!uid_entry || uid_entry->uid != uid)
uid_entry = find_or_register_uid(uid); // UIDエントリの探索と登録
if (!uid_entry)
continue;
add_uid_io_stats(uid_entry, task, UID_STATE_TOTAL_CURR); // 取得したUIDエントリの再計算
} while_each_thread(temp, task);
rcu_read_unlock();
/* ・・・ */
}
以下のように、task_struct構造体にI/O統計計情報がすでに忍ばせてあり、それを属するuid_entryのUID_STATE_TOTAL_CURRに加算する。
task_struct構造体が保持するI/O統計はLinuxオリジナルのtask_io_accountingの仕組みで実装されており、proc/[pid]/ioで確認できる情報である。
また、task_entry自体も同じく更新している。
static void add_uid_io_stats(struct uid_entry *uid_entry,
struct task_struct *task, int slot)
{
struct io_stats *io_slot = &uid_entry->io[slot];
io_slot->read_bytes += task->ioac.read_bytes;
io_slot->write_bytes += compute_write_bytes(task);
io_slot->rchar += task->ioac.rchar;
io_slot->wchar += task->ioac.wchar;
io_slot->fsync += task->ioac.syscfs;
add_uid_tasks_io_stats(uid_entry, task, slot); // task_entry側の更新
}
static void add_uid_tasks_io_stats(struct uid_entry *uid_entry,
struct task_struct *task, int slot)
{
struct task_entry *task_entry = find_or_register_task(uid_entry, task);
struct io_stats *task_io_slot = &task_entry->io[slot];
task_io_slot->read_bytes += task->ioac.read_bytes;
task_io_slot->write_bytes += compute_write_bytes(task);
task_io_slot->rchar += task->ioac.rchar;
task_io_slot->wchar += task->ioac.wchar;
task_io_slot->fsync += task->ioac.syscfs;
}
全UID/taskの現時点の統計情報が取れた。
次に、現在の状態(forground or background)に対応する統計情報へ加算する。
現時点の統計(UID_STATE_TOTAL_CURR) + 終了したのtaskの統計(UID_STATE_DEAD_TASKS) - 更新前の統計(UID_STATE_TOTAL_LAST)が、更新前後での増加分となる。
それを現在の状態に対応するio_stats(UID_STATE_FOREGROUND or UID_STATE_BACKGROUND)へ加算することで完了する。
#終了したtaskに関する統計情報は 3. process_notifier() にて説明。
static void update_io_stats_all_locked(void)
{
/* ・・・ */
hash_for_each(hash_table, bkt, uid_entry, hash) { // 全UIDに対して
compute_io_bucket_stats(&uid_entry->io[uid_entry->state], // 現在の状態に対応するio_statsを更新。
&uid_entry->io[UID_STATE_TOTAL_CURR],
&uid_entry->io[UID_STATE_TOTAL_LAST],
&uid_entry->io[UID_STATE_DEAD_TASKS]);
compute_io_uid_tasks(uid_entry);
}
}
static void compute_io_bucket_stats(struct io_stats *io_bucket,
struct io_stats *io_curr,
struct io_stats *io_last,
struct io_stats *io_dead)
{
/* tasks could switch to another uid group, but its io_last in the
* previous uid group could still be positive.
* therefore before each update, do an overflow check first
*/
int64_t delta;
delta = io_curr->read_bytes + io_dead->read_bytes -
io_last->read_bytes;
io_bucket->read_bytes += delta > 0 ? delta : 0;
delta = io_curr->write_bytes + io_dead->write_bytes -
io_last->write_bytes;
io_bucket->write_bytes += delta > 0 ? delta : 0;
delta = io_curr->rchar + io_dead->rchar - io_last->rchar;
io_bucket->rchar += delta > 0 ? delta : 0;
delta = io_curr->wchar + io_dead->wchar - io_last->wchar;
io_bucket->wchar += delta > 0 ? delta : 0;
delta = io_curr->fsync + io_dead->fsync - io_last->fsync;
io_bucket->fsync += delta > 0 ? delta : 0;
io_last->read_bytes = io_curr->read_bytes; // 次の更新に備えて更新前の値を残しておく。
io_last->write_bytes = io_curr->write_bytes;
io_last->rchar = io_curr->rchar;
io_last->wchar = io_curr->wchar;
io_last->fsync = io_curr->fsync;
memset(io_dead, 0, sizeof(struct io_stats));
}
task_entryも以下により同じく更新。
static void compute_io_uid_tasks(struct uid_entry *uid_entry)
{
struct task_entry *task_entry;
unsigned long bkt_task;
hash_for_each(uid_entry->task_entries, bkt_task, task_entry, hash) {
compute_io_bucket_stats(&task_entry->io[uid_entry->state],
&task_entry->io[UID_STATE_TOTAL_CURR],
&task_entry->io[UID_STATE_TOTAL_LAST],
&task_entry->io[UID_STATE_DEAD_TASKS]);
}
}
以上により、全UID/taskのFOREGROUNDかBACKGROUNDの統計情報の更新が完了する。
2. uid_procstat_write()
/proc/uid_procstat/setへのwrite時に実行される。
この関数の目的は、インターフェース要件から、指定のUIDのbackgroundとforegroundを切り替える際にwriteされる様子。
実際の処理としては、指定されたUIDに対するuid_entryのstateのforeground/backgroundを切り替える。
ただし切り替える前に、前の状態のio_statsを再計算しておく。
static ssize_t uid_procstat_write(struct file *file,
const char __user *buffer, size_t count, loff_t *ppos)
{
struct uid_entry *uid_entry;
uid_t uid;
int argc, state;
char input[128];
if (count >= sizeof(input))
return -EINVAL;
if (copy_from_user(input, buffer, count))
return -EFAULT;
input[count] = '\0';
argc = sscanf(input, "%u %d", &uid, &state);
if (argc != 2)
return -EINVAL;
if (state != UID_STATE_BACKGROUND && state != UID_STATE_FOREGROUND)
return -EINVAL;
rt_mutex_lock(&uid_lock);
uid_entry = find_or_register_uid(uid); // // UIDエントリの探索と登録
if (!uid_entry) {
rt_mutex_unlock(&uid_lock);
return -EINVAL;
}
if (uid_entry->state == state) { // stateが変わらないなら何もしない
rt_mutex_unlock(&uid_lock);
return count;
}
update_io_stats_uid_locked(uid_entry); // state切り替え前に一度再計算
uid_entry->state = state; // 新しいstateへ書き換え
rt_mutex_unlock(&uid_lock);
return count;
}
static void update_io_stats_uid_locked(struct uid_entry *uid_entry)
{
struct task_struct *task, *temp;
struct user_namespace *user_ns = current_user_ns();
memset(&uid_entry->io[UID_STATE_TOTAL_CURR], 0,
sizeof(struct io_stats));
set_io_uid_tasks_zero(uid_entry);
rcu_read_lock();
do_each_thread(temp, task) { // 全taskに対して
if (from_kuid_munged(user_ns, task_uid(task)) != uid_entry->uid) // 指定されたUIDに属するtaskであれば
continue;
add_uid_io_stats(uid_entry, task, UID_STATE_TOTAL_CURR); // taskの統計情報をUIDへ加算
} while_each_thread(temp, task);
rcu_read_unlock();
compute_io_bucket_stats(&uid_entry->io[uid_entry->state], // 切り替え前のstateのio_statsを再計算
&uid_entry->io[UID_STATE_TOTAL_CURR],
&uid_entry->io[UID_STATE_TOTAL_LAST],
&uid_entry->io[UID_STATE_DEAD_TASKS]);
compute_io_uid_tasks(uid_entry);
}
3. process_notifier()
PROFILE_TASK_EXITイベントに対するnortifier chainのハンドラ。taskの終了時(=processの終了時)に発生する。
この関数の目的は、UID_STATE_DEAD_TASKSのカウント。
引数として渡されるtask_structは終了するtaskのI/O統計情報であるため、対応するtask_entryのUID_STATE_DEAD_TASKSに加算する。
profile_event_register(PROFILE_TASK_EXIT, &process_notifier_block);
static int process_notifier(struct notifier_block *self,
unsigned long cmd, void *v)
{
struct task_struct *task = v;
struct uid_entry *uid_entry;
u64 utime, stime;
uid_t uid;
if (!task)
return NOTIFY_OK;
rt_mutex_lock(&uid_lock);
uid = from_kuid_munged(current_user_ns(), task_uid(task));
uid_entry = find_or_register_uid(uid);
if (!uid_entry) {
pr_err("%s: failed to find uid %d\n", __func__, uid);
goto exit;
}
task_cputime_adjusted(task, &utime, &stime);
uid_entry->utime += utime;
uid_entry->stime += stime;
add_uid_io_stats(uid_entry, task, UID_STATE_DEAD_TASKS);
exit:
rt_mutex_unlock(&uid_lock);
return NOTIFY_OK;
}
4. uid_cputime_show()
/proc/uid_cputime/show_uid_statのopen時に実行される。
この関数の目的は、インターフェース要件から、
UID のプロセスがユーザー空間で実行された時間とカーネル空間で実行された時間(読み取り専用ファイル)。
を取得する際に利用する。
同じhash_table上に実行時間の情報を保持しているためfind_or_register_uid()が利用されるが、I/O統計情報の更新は行われないので割愛する。
まとめ
/proc/uid_io/statsに関する概要をソースレベルで大まかに確認した。
次回はstoraged側の処理や、task_io_accountingの仕組みによりどのように統計情報が蓄積されていくのかを確認していきたい。
参考文献
Implementing storaged
ASB-2020-08-05_4.19-q-release
インターフェース要件