procfsのtask statは何を示すのか

2015/12/12 linux

Advent calender2015

今年も書かせていただくことにしました。比較的まともに?記事を書くことのできるトリガーとして助かります。

Linux kernel vanilla v4.2

本記事では、著者の技量の都合により語彙が適当では無いことがあります。指摘いただけますと助かります。


procfsで見えるもの

稼働しているシステムで動いているプロセスを調べるために、psコマンドを使っていますよね。busybox版のソースコードを見ると判りやすいのですが、/proc配下のファイルを参照して情報を集めているのです。/procは、procfsがマウントされており、物理記憶装置上のファイルシステムではなく、Linux kernelとユーザランドとをつないでいるインタフェースでもあります。

タスク情報

ユーザランドでは、プロセス、スレッドと区別していますが、kernelではいずれも"task struct"で管理されます。スレッドは、libc実装ではプロセス空間を共有するtaskと考えるとよいでしょう。forkするときは注意が必要なのですが、これはまた別の機会があればそちらで。。。

本題のプロセス情報、以下のディレクトリ配下にあるファイルから取得ができます。

/proc/<id>/

このファイルを通して読み書きされるコードは、procfsのところでも触れたとおり、kernelのフレームワークを利用すると簡潔なコードになります。とりあえずソースコードを見てみましょう:

FILE: fs/proc/base.c

static const struct pid_entry tid_base_stuff[] = {
	ONE("stat",      S_IRUGO, proc_tid_stat),
	ONE("statm",     S_IRUGO, proc_pid_statm),
	REG("maps",      S_IRUGO, proc_tid_maps_operations),

statにpsで表示するための情報がたくさん含まれているようです(busybox版psコマンドソースコードより):

int proc_tid_stat(struct seq_file *m, struct pid_namespace *ns,
			struct pid *pid, struct task_struct *task)
{
	return do_task_stat(m, ns, pid, task, 0);
}

FILE: fs/proc/array.c

static int do_task_stat(struct seq_file *m, struct pid_namespace *ns,
			struct pid *pid, struct task_struct *task, int whole)

ここをざーっと眺めていくと、なにを表示しているのかがわかりますね!(ブン投げ

/proc/PID/statの表示コード(変数はこの手前に代入されている)

	seq_printf(m, "%d (%s) %c", pid_nr_ns(pid, ns), tcomm, state);
	seq_put_decimal_ll(m, ' ', ppid);
	seq_put_decimal_ll(m, ' ', pgid);
	seq_put_decimal_ll(m, ' ', sid);
	seq_put_decimal_ll(m, ' ', tty_nr);
	seq_put_decimal_ll(m, ' ', tty_pgrp);
	seq_put_decimal_ull(m, ' ', task->flags);
	seq_put_decimal_ull(m, ' ', min_flt);
	seq_put_decimal_ull(m, ' ', cmin_flt);
	seq_put_decimal_ull(m, ' ', maj_flt);
	seq_put_decimal_ull(m, ' ', cmaj_flt);
	seq_put_decimal_ull(m, ' ', cputime_to_clock_t(utime));
	seq_put_decimal_ull(m, ' ', cputime_to_clock_t(stime));
	seq_put_decimal_ll(m, ' ', cputime_to_clock_t(cutime));
	seq_put_decimal_ll(m, ' ', cputime_to_clock_t(cstime));
	seq_put_decimal_ll(m, ' ', priority);
	seq_put_decimal_ll(m, ' ', nice);
	seq_put_decimal_ll(m, ' ', num_threads);
	seq_put_decimal_ull(m, ' ', 0);
	seq_put_decimal_ull(m, ' ', start_time);
	seq_put_decimal_ull(m, ' ', vsize);
	seq_put_decimal_ull(m, ' ', mm ? get_mm_rss(mm) : 0);
	seq_put_decimal_ull(m, ' ', rsslim);
	seq_put_decimal_ull(m, ' ', mm ? (permitted ? mm->start_code : 1) : 0);
	seq_put_decimal_ull(m, ' ', mm ? (permitted ? mm->end_code : 1) : 0);
	seq_put_decimal_ull(m, ' ', (permitted && mm) ? mm->start_stack : 0);
	seq_put_decimal_ull(m, ' ', esp);
	seq_put_decimal_ull(m, ' ', eip);
	/* The signal information here is obsolete.
	 * It must be decimal for Linux 2.0 compatibility.
	 * Use /proc/#/status for real-time signals.
	 */
	seq_put_decimal_ull(m, ' ', task->pending.signal.sig[0] & 0x7fffffffUL);
	seq_put_decimal_ull(m, ' ', task->blocked.sig[0] & 0x7fffffffUL);
	seq_put_decimal_ull(m, ' ', sigign.sig[0] & 0x7fffffffUL);
	seq_put_decimal_ull(m, ' ', sigcatch.sig[0] & 0x7fffffffUL);
	seq_put_decimal_ull(m, ' ', wchan);
	seq_put_decimal_ull(m, ' ', 0);
	seq_put_decimal_ull(m, ' ', 0);
	seq_put_decimal_ll(m, ' ', task->exit_signal);
	seq_put_decimal_ll(m, ' ', task_cpu(task));
	seq_put_decimal_ull(m, ' ', task->rt_priority);
	seq_put_decimal_ull(m, ' ', task->policy);
	seq_put_decimal_ull(m, ' ', delayacct_blkio_ticks(task));
	seq_put_decimal_ull(m, ' ', cputime_to_clock_t(gtime));
	seq_put_decimal_ll(m, ' ', cputime_to_clock_t(cgtime));

	if (mm && permitted) {
		seq_put_decimal_ull(m, ' ', mm->start_data);
		seq_put_decimal_ull(m, ' ', mm->end_data);
		seq_put_decimal_ull(m, ' ', mm->start_brk);
		seq_put_decimal_ull(m, ' ', mm->arg_start);
		seq_put_decimal_ull(m, ' ', mm->arg_end);
		seq_put_decimal_ull(m, ' ', mm->env_start);
		seq_put_decimal_ull(m, ' ', mm->env_end);
	} else
		seq_printf(m, " 0 0 0 0 0 0 0");

	if (permitted)
		seq_put_decimal_ll(m, ' ', task->exit_code);
	else
		seq_put_decimal_ll(m, ' ', 0);

	seq_putc(m, '\n');
	if (mm)
		mmput(mm);

pid_nr_ns()では、namespaceで管理しているpidに変換する関数のようです。namespaceも、不勉強なため説明できるような理解はできていません。

ぼちぼち調べながら見ていきましょう... schedulerが参照するようなcpu timeとかあるので、ここだけ見ても意味は理解できない雰囲気ですね。


format vars description In the kernel space
%dpidプロセスIDpid_nr_ns(pid, ns)
%stcommCOMM名(task_struct*)->comm
%cstate状態フラグのうち最も若いフラグの立っているところ([RSDTtXz]:左が若い)get_task_state()の1文字目
(ll)ppid親プロセスIDtask_tgid_nr_ns(task->real_parent, ns)
(ll)pgidプロセスグループIDtask_pgrp_nr_ns(task, ns)
(ll)sidグループリーダID(?)task_session_nr_ns(task, ns), task->group_leaderのpid
(ll)tty_nrttyに属していれば、tty drviceのidを入れる.なければ0((minor & ~0xFF)<<12) or (major << 8) or ((minor&0xFF)
(ll)tty_pgrpttyのプロセスグループ-
(ull)task->flagsプロセス状態を示すビットマップ(後述)-
(ull)min_fltminor faultカウントの総和(struct task_struct*)->min_flt
(ull)cmin_fltsignal.minor faultカウントの総和(struct signal_struct)->cmin_flt
(ull)maj_fltmajor faultカウントの総和(struct task_struct*)->maj_flt
(ull)cmaj_fltsignal.major faultカウントの総和(struct signal_struct)->cmaj_flt
(ull)utimeユーザランドに費やしたcpu timecputime_to_clock_t(utime)
(ull)stimeシステムに費やしたcpu timecputime_to_clock_t(stime)
(ll)cutime亡くなった子プロセスを含めたユーザランドに費やしたcpu timecputime_to_clock_t((struct signal_struct)->cutime)
(ll)cstime亡くなった子プロセスを含めたシステムに費やしたcpu timecputime_to_clock_t((struct signal_struct)->cstime)
(ll)prioritytask priority()(->prio - 100)
(ll)nicenice値(->static_prio - 120(DEFAULT_PRIO))
(ll)num_threadsスレッド数tsk->signal->nr_threads
(ull)0 (fixed.)--
(ull)start_timeboot based time in ticks, copy_proces()された時nsec_to_clock_t(task->real_start_time)
(ull)vsizeマップされたサイズ[Bytes]PAGE_SIZE * mm->total_vm
(ull)mm->rss_stat.count[ MM_FILEPAGES,MM_ANONPAGES]の和プロセスに割り当てられている物理ページ数(mm ? get_mm_rss(mm) : 0)
(ull)rsslimmax resident set sizesig->rlim[RLIMIT_RSS].rlim_cur
(ull)このプロセスのコード領域の先頭アドレスmm ? (permitted ? mm->start_code : 1) : 0
(ull)このプロセスのコード領域の終了アドレスmm ? (permitted ? mm->end_code : 1) : 0
(ull)スタックアドレスの先頭(スタックの底?)(permitted && mm) ? mm->start_stack : 0
(ull)espstack pointer-(arch依存)
(ull)eipindex pointer?-(arch依存)
(ull)ペンディングされているシグナルのビットマップtask->pending.signal.sig[0] & 0x7fffffffUL
(ull)(確認中)task->blocked.sig[0] & 0x7fffffffUL
(ull)(確認中)sigign.sig[0] & 0x7fffffffUL
(ull)(確認中)sigcatch.sig[0] & 0x7fffffffUL
(ull)wchan待ち状態に入った関数名(名前なし設定だとアドレス)get_wchan(task)
(ull)0 (fixed)--
(ull)0 (fixed)--
(ll)task->exit_signal(確認中)-
(ll)task_cpu(task)(確認中)-
(ull)task->rt_priority(確認中)-
(ull)task->policy(確認中)-
(ull)delayacct_blkio_ticks(task)(確認中)-
(ull)cputime_to_clock_t(gtime)(確認中)-
(ll)cputime_to_clock_t(cgtime)(確認中)-
(ull)mm->start_data(確認中)-
(ull)mm->end_data(確認中)-
(ull)mm->start_brk(確認中)-
(ull)mm->arg_start(確認中)-
(ull)mm->arg_end(確認中)-
(ull)mm->env_start(確認中)-
(ull)mm->env_end(確認中)-
(ll)task->exit_code0 if !permitted(確認中)

コメントも面白いですね。RT-signalは statusを見ろって書いてます。

	/* The signal information here is obsolete.
	 * It must be decimal for Linux 2.0 compatibility.
	 * Use /proc/#/status for real-time signals.
	 */

※per proces flags

/*
 * Per process flags
 */
#define PF_EXITING	0x00000004	/* getting shut down */
#define PF_EXITPIDONE	0x00000008	/* pi exit done on shut down */
#define PF_VCPU		0x00000010	/* I'm a virtual CPU */
#define PF_WQ_WORKER	0x00000020	/* I'm a workqueue worker */
#define PF_FORKNOEXEC	0x00000040	/* forked but didn't exec */
#define PF_MCE_PROCESS  0x00000080      /* process policy on mce errors */
#define PF_SUPERPRIV	0x00000100	/* used super-user privileges */
#define PF_DUMPCORE	0x00000200	/* dumped core */
#define PF_SIGNALED	0x00000400	/* killed by a signal */
#define PF_MEMALLOC	0x00000800	/* Allocating memory */
#define PF_NPROC_EXCEEDED 0x00001000	/* set_user noticed that RLIMIT_NPROC was exceeded */
#define PF_USED_MATH	0x00002000	/* if unset the fpu must be initialized before use */
#define PF_USED_ASYNC	0x00004000	/* used async_schedule*(), used by module init */
#define PF_NOFREEZE	0x00008000	/* this thread should not be frozen */
#define PF_FROZEN	0x00010000	/* frozen for system suspend */
#define PF_FSTRANS	0x00020000	/* inside a filesystem transaction */
#define PF_KSWAPD	0x00040000	/* I am kswapd */
#define PF_MEMALLOC_NOIO 0x00080000	/* Allocating memory without IO involved */
#define PF_LESS_THROTTLE 0x00100000	/* Throttle me less: I clean memory */
#define PF_KTHREAD	0x00200000	/* I am a kernel thread */
#define PF_RANDOMIZE	0x00400000	/* randomize virtual address space */
#define PF_SWAPWRITE	0x00800000	/* Allowed to write to swap */
#define PF_NO_SETAFFINITY 0x04000000	/* Userland is not allowed to meddle with cpus_allowed */
#define PF_MCE_EARLY    0x08000000      /* Early kill for mce process policy */
#define PF_MUTEX_TESTER	0x20000000	/* Thread belongs to the rt mutex tester */
#define PF_FREEZER_SKIP	0x40000000	/* Freezer should not count it as freezable */
#define PF_SUSPEND_TASK 0x80000000      /* this thread called freeze_processes and should not be frozen */



cmin_fltとかの説明。統計情報ですね...

		/*
		 * The resource counters for the group leader are in its
		 * own task_struct.  Those for dead threads in the group
		 * are in its signal_struct, as are those for the child
		 * processes it has previously reaped.  All these
		 * accumulate in the parent's signal_struct c* fields.
		 *
		 * We don't bother to take a lock here to protect these
		 * p->signal fields because the whole thread group is dead
		 * and nobody can change them.
		 *
		 * psig->stats_lock also protects us from our sub-theads
		 * which can reap other children at the same time. Until
		 * we change k_getrusage()-like users to rely on this lock
		 * we have to take ->siglock as well.
		 *
		 * We use thread_group_cputime_adjusted() to get times for
		 * the thread group, which consolidates times for all threads
		 * in the group including the group leader.
		 */

prio : RT[0..99], user [100..140], 120がnice=0, user-nice = -20..+19 => 100..139


とりあえずここまで。。。本当はpsコマンドでタスク状態をモニタして、不具合や性能問題の追跡例を書いてみようかと思っていました。それよりも、ユーザランドからプロセスの状態を見る方法を示したほうが応用範囲が広がるだろうと思われます。

一部未確認がありますが、年内には更新を終わらせたいと思います。。。

LinuxのARMv7でのMMU faultを追いかける

2014/12/21 linux::ARM

Linux(ARMv7系)のメモリ管理を追ってみる

Linux Advent Calendar 2014の21日目の記事です。

kernel sourceの追いかけかた

前回のcopy_to_user()のfault処理実装を見たときに、__do_kernel_fault()あたりから先はちらっと見たので、そもそもメモリ管理全般はどうなっているのだろう、と気になって仕方が無くなる。
とりあえず下から覗いていくのが低レイヤー民(そんなものはない)のたしなみだろう、ということで、最近メジャーなARMv7アーキテクチャをターゲットとして調査する。

kernelのメモリ管理の基本と、ARMアーキテクチャでの実装を確認し、faultハンドラを追いかけていこう。

追いかけ方は人それぞれかもしれないけれど、Makefileにtagsがターゲットとして存在しているので、マクロやプリプロセッサが多いので、適当なターゲットでkernel sourceをbuildしておき、tagsも作っておくと楽ができる。マクロで関数を生成しているところもあるので、すべてタグジャンプできるとは限らないので注意も必要だ...
迷ったらバイナリを逆アセンブルするとかすればヒントにもなろう。
それでもだめならgrepするなり舐めるなりするといい(?)。

メモリ管理の基本

gorman氏のUnderstanding the Linux Virtual Memory Managerを見ればだいたい判るような気がしてきた。こんなトコロ読むよりも有意だと思う(´・ω・)

ココを参照されている諸兄は、すでにカーネル空間とユーザ空間とかの存在は把握されていると思います。
で、Virtual AddressをPhysical Addressに変換する機能はARMv7Aの場合はMMUが物理メモリを参照して勝手に変換しちゃう。
ということまで理解しているとしましょう。

Linuxの場合、基本的に4kByte/pageを使うことにしてあるので、第二レベル変換テーブルはsmall sectionが選択されています。
必然的に、第一レベル変換はSection Baseの変換テーブルを用いることになります。
Section baseの場合は、1MiBごとに第二レベル変換テーブルの先頭アドレスが置かれます。

順序が逆になりますが、Linuxのメモリ管理テーブルについて文字だけでまとめます。
論理アドレスを上位ビットからテーブル引きをして、PGD(Page Global Directory ),
PUD(Page Upper Directory), PMD(Page Middle Directory), PTE(Page Table Entries )の順に
4段階でページテーブルをインデックスします。
先に示した資料ではPUDが無いので、どこかで追加されたのでしょう。

linux/mm/配下は、この概念をベースにして実装されており、アーキテクチャに依存する
アドレス空間の差異を吸収できる仕組みになっています。
Linuxの論理-物理変換管理テーブルが定義されており、汎用性を持った記述になっています。
アーキテクチャ依存部でうまくコードを共通化しているのでそのあたりも見ていきましょう。
1. VAを元に PGDから PUDの先頭アドレスを得る。
2. PUDとVAから PMDの先頭アドレスを得る(arm32ではPUDは未使用,PGD=>PMDとなっている)
3. PMDとVAから PTEの先頭アドレスを得る(PGDがPMDをインデックスしている、と見る)。
4. PTEとVAから 該当するページの物理アドレスや属性や状態を得る。

arm32/LPAEなしでは、pgdレベル1変換テーブル、PMDがレベル2変換テーブル、
pteがレベル2変換テーブルの該当アドレスにあたるSmall page要素になります。
PUD,PMDの取得は、pud_offset()とpmd_offset()で実装されますが、pgd,pudをそのまま返しています。
関連するシンボルは以下のとおりで、ビット幅をセットします。
symbol value
PAGE_SHIFT12(4k)
PMD_SHIFT21
SECTION_SHIFT20
SUPERSECTION_SHIFT20
下位12bitはページ単位になるので、変換テーブルによって指される最小単位となります。
Cortex-Aシリーズになると、不要な気がしますが、過去の経緯でPGDは2GiB空間をインデックスするように設計されています。
accessed/dirty bitがpteに存在しないので それをエミュレートするため, とありますが,
ここは消化しきれていません. arm依存部でも ARMv4,v5,v7,v8と差異はそこそこありそうです。


ARMv7Aの場合

ここで、LAPEなしと仮定して、以下を参照する(LAPEありなら03level)。

FILE:arch/arm/include/asm/pgtable-2level.h
/*
 * Hardware-wise, we have a two level page table structure, where the first
 * level has 4096 entries, and the second level has 256 entries.  Each entry
 * is one 32-bit word.  Most of the bits in the second level entry are used
 * by hardware, and there aren't any "accessed" and "dirty" bits.
 *
 * Linux on the other hand has a three level page table structure, which can
 * be wrapped to fit a two level page table structure easily - using the PGD
 * and PTE only.  However, Linux also expects one "PTE" table per page, and
 * at least a "dirty" bit.
 *
 * Therefore, we tweak the implementation slightly - we tell Linux that we
 * have 2048 entries in the first level, each of which is 8 bytes (iow, two
 * hardware pointers to the second level.)  The second level contains two
 * hardware PTE tables arranged contiguously, preceded by Linux versions
 * which contain the state information Linux needs.  We, therefore, end up
 * with 512 entries in the "PTE" level.
 *
 * This leads to the page tables having the following layout:
 *
 *    pgd             pte
 * |        |
 * +--------+
 * |        |       +------------+ +0
 * +- - - - +       | Linux pt 0 |
 * |        |       +------------+ +1024
 * +--------+ +0    | Linux pt 1 |
 * |        |-----> +------------+ +2048
 * +- - - - + +4    |  h/w pt 0  |
 * |        |-----> +------------+ +3072
 * +--------+ +8    |  h/w pt 1  |
 * |        |       +------------+ +4096
 *
 * See L_PTE_xxx below for definitions of bits in the "Linux pt", and
 * PTE_xxx for definitions of bits appearing in the "h/w pt".
 *
 * PMD_xxx definitions refer to bits in the first level page table.
 *
 * The "dirty" bit is emulated by only granting hardware write permission
 * iff the page is marked "writable" and "dirty" in the Linux PTE.  This
 * means that a write to a clean page will cause a permission fault, and
 * the Linux MM layer will mark the page dirty via handle_pte_fault().
 * For the hardware to notice the permission change, the TLB entry must
 * be flushed, and ptep_set_access_flags() does that for us.
 *
 * The "accessed" or "young" bit is emulated by a similar method; we only
 * allow accesses to the page if the "young" bit is set.  Accesses to the
 * page will cause a fault, and handle_pte_fault() will set the young bit
 * for us as long as the page is marked present in the corresponding Linux
 * PTE entry.  Again, ptep_set_access_flags() will ensure that the TLB is
 * up to date.
 *
 * However, when the "young" bit is cleared, we deny access to the page
 * by clearing the hardware PTE.  Currently Linux does not flush the TLB
 * for us in this case, which means the TLB will retain the transation
 * until either the TLB entry is evicted under pressure, or a context
 * switch which changes the user space mapping occurs.
 */
#define PTRS_PER_PTE            512
#define PTRS_PER_PMD            1
#define PTRS_PER_PGD            2048

#define PTE_HWTABLE_PTRS        (PTRS_PER_PTE)
#define PTE_HWTABLE_OFF         (PTE_HWTABLE_PTRS * sizeof(pte_t))
#define PTE_HWTABLE_SIZE        (PTRS_PER_PTE * sizeof(u32))

/*
 * PMD_SHIFT determines the size of the area a second-level page table can map
 * PGDIR_SHIFT determines what a third-level page table entry can map
 */
#define PMD_SHIFT               21
#define PGDIR_SHIFT             21

#define PMD_SIZE                (1UL << PMD_SHIFT)
#define PMD_MASK                (~(PMD_SIZE-1))
#define PGDIR_SIZE              (1UL << PGDIR_SHIFT)
#define PGDIR_MASK              (~(PGDIR_SIZE-1))

FILE: arch/arm/include/asm/pgtable.h
/* to find an entry in a page-table-directory */
#define pgd_index(addr)		((addr) >> PGDIR_SHIFT)  /// 21bit@LAPEなし

#define pgd_offset(mm, addr)	((mm)->pgd + pgd_index(addr))

/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(addr)	pgd_offset(&init_mm, addr)


static inline pte_t *pmd_page_vaddr(pmd_t pmd)
{
	return __va(pmd_val(pmd) & PHYS_MASK & (s32)PAGE_MASK);
}

#ifndef CONFIG_HIGHPTE
#define __pte_map(pmd)		pmd_page_vaddr(*(pmd))
#define __pte_unmap(pte)	do { } while (0)
#else
#endif
#define pte_index(addr)		(((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pte_offset_kernel(pmd,addr)	(pmd_page_vaddr(*(pmd)) + pte_index(addr))
#define pte_offset_map(pmd,addr)	(__pte_map(pmd) + pte_index(addr))
#define pte_unmap(pte)			__pte_unmap(pte)

#define pte_pfn(pte)		((pte_val(pte) & PHYS_MASK) >> PAGE_SHIFT)
#define pfn_pte(pfn,prot)	__pte(__pfn_to_phys(pfn) | pgprot_val(prot))

#define pte_page(pte)		pfn_to_page(pte_pfn(pte))
#define mk_pte(page,prot)	pfn_pte(page_to_pfn(page), prot)

#define pte_clear(mm,addr,ptep)	set_pte_ext(ptep, __pte(0), 0)

#define pte_none(pte)		(!pte_val(pte))
#define pte_present(pte)	(pte_val(pte) & L_PTE_PRESENT)
#define pte_write(pte)		(!(pte_val(pte) & L_PTE_RDONLY))
#define pte_dirty(pte)		(pte_val(pte) & L_PTE_DIRTY)
#define pte_young(pte)		(pte_val(pte) & L_PTE_YOUNG)
#define pte_exec(pte)		(!(pte_val(pte) & L_PTE_XN))
#define pte_special(pte)	(0)
以上がソースを追うのに必要な情報(だったはず)。

具体的に変換テーブルはどこに..?

見ていて有用なのがデバッグ用の関数やコメント。
Oopsを吐くときに使用されている show_pte()から依存部の実装を追える。

FILE: arch/arm/mm/fault.c
/*
 * This is useful to dump out the page tables associated with
 * 'addr' in mm 'mm'.
 */
void show_pte(struct mm_struct *mm, unsigned long addr)
{
	pgd_t *pgd;

	if (!mm)
		mm = &init_mm;

	printk(KERN_ALERT "pgd = %p\n", mm->pgd);
	pgd = pgd_offset(mm, addr);
	printk(KERN_ALERT "[%08lx] *pgd=%08llx",
			addr, (long long)pgd_val(*pgd));
...
第一レベル変換テーブルが mm_structのメンバpgdであることがわかる。
引数mmがNULLの場合、すなわちタスクのmmが無い状態では、init_mmが基底になっていそうである*1

このinit_mmを探してみると以下に見つかる。

FILE:mm/init-mm.c
struct mm_struct init_mm = {
        .mm_rb          = RB_ROOT,
        .pgd            = swapper_pg_dir,
        .mm_users       = ATOMIC_INIT(2),
        .mm_count       = ATOMIC_INIT(1),
        .mmap_sem       = __RWSEM_INITIALIZER(init_mm.mmap_sem),
        .page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
        .mmlist         = LIST_HEAD_INIT(init_mm.mmlist),
        INIT_MM_CONTEXT(init_mm)
};
これ(swapper_pg_dir)の実態はどこにあるのか。
FILE:arch/arm/kernel/head.S
/*
 * swapper_pg_dir is the virtual address of the initial page table.
 * We place the page tables 16K below KERNEL_RAM_VADDR.  Therefore, we must
 * make sure that KERNEL_RAM_VADDR is correctly set.  Currently, we expect
 * the least significant 16 bits to be 0x8000, but we could probably
 * relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000.
 */

#define KERNEL_RAM_VADDR        (PAGE_OFFSET + TEXT_OFFSET)
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif

#ifdef CONFIG_ARM_LPAE
        /* LPAE requires an additional page for the PGD */
#define PG_DIR_SIZE     0x5000
#define PMD_ORDER       3
#else
#define PG_DIR_SIZE     0x4000
#define PMD_ORDER       2
#endif

        .globl  swapper_pg_dir
        .equ    swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE

        .macro  pgtbl, rd, phys
        add     \rd, \phys, #TEXT_OFFSET - PG_DIR_SIZE
        .endm
MMUのテーブルウォークに使われる、レベル1変換テーブルをswapper_pg_dirに格納している。
しかし、2MiB/1stLvとして運用している(前引用のコメントどおり)。
PGD1MiB単位のブロック.. 1MiB x 4Ki個(12bit)のテーブルで 4GiBフルアドレッシング
PTE最終的に4KiBをインデックスして 256個(8bit)テーブルがあれば 1MiBになる。


pgd_offset_k()でインデックスを求めるが、これは2MiB単位でアドレスを求める。
pgdが2MiBになる理由が未だわからんなぁ。
テーブルは作ってしまうのに、どこに属性を置くのか...

タスクがある場合
SMPの場合は cpu固有情報は cpu_なんとか()という関数名で実装されているようである。
第一レベル変換テーブル、すなわちpgd配列の先頭ポインタを取得する関数を見てみると、
以下の実相となっていた。
FILE: arch/arm/include/asm/proc-fns.h
#ifdef CONFIG_ARM_LPAE
#else
#define cpu_get_pgd()   \
        ({                                              \
                unsigned long pg;                       \
                __asm__("mrc    p15, 0, %0, c2, c0, 0"  \
                         : "=r" (pg) : : "cc");         \
                pg &= ~0x3fff;                          \
                (pgd_t *)phys_to_virt(pg);              \
        })
#endif
意訳すると、"Translation Table Base Register 0"を取得する
(=current taskのpgd先頭アドレス(物理)を取得して、論理に変換する)と言えよう。


*1 : タスク生成時のメモリアロケーションを調べておく必要がありますなぁ

で?

MMU faultのハンドラは以下で、割り込みベクタまわりの処理から呼ばれてくる。
FILE: arch/mm/fault.c
static int __kprobes
do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
...
	tsk = current;
	mm  = tsk->mm;
...
	/*
	 * If we're in an interrupt or have no user
	 * context, we must not take the fault..
	 */
	if (in_atomic() || !mm)
		goto no_context;
...
	fault = __do_page_fault(mm, addr, fsr, flags, tsk);
....
	if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP | VM_FAULT_BADACCESS))))
		return 0;
...
	__do_user_fault(tsk, addr, fsr, sig, code, regs);
	return 0;

no_context:
	__do_kernel_fault(mm, addr, fsr, regs);
	return 0;

ユーザ空間

static int __kprobes
__do_page_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
		unsigned int flags, struct task_struct *tsk)
{
	struct vm_area_struct *vma;
	int fault;

	vma = find_vma(mm, addr);
	fault = VM_FAULT_BADMAP;
	if (unlikely(!vma))
		goto out;
	if (unlikely(vma->vm_start > addr))
		goto check_stack;

	/*
	 * Ok, we have a good vm_area for this
	 * memory access, so we can handle it.
	 */
good_area:
	if (access_error(fsr, vma)) {
		fault = VM_FAULT_BADACCESS;
		goto out;
	}

	return handle_mm_fault(mm, vma, addr & PAGE_MASK, flags);

check_stack:
	/* Don't allow expansion below FIRST_USER_ADDRESS */
	if (vma->vm_flags & VM_GROWSDOWN &&
	    addr >= FIRST_USER_ADDRESS && !expand_stack(vma, addr))
		goto good_area;
out:
	return fault;
}
ここはユーザ空間でのfaultで、タスクに割り当てられた空間かどうかをチェックしています。
find_vma()がそれで、mm_struct構造体メンバのmmapをスキャンします(curent->vmacache[]を先に見る)。
⇒タスクごとのメモリ管理、割り当てと解放、その他はこの変数を触っている個所を見ればよいと分かりました。

handle_mm_fault()はアーキテクチャに依存しないソースで実装されていますので、まずはここまで。
エラー発生時
SIGSEGVとかですね.. 引数sigに エラー判定結果を入れて呼ばれます。
/*
 * Something tried to access memory that isn't in our memory map..
 * User mode accesses just cause a SIGSEGV
 */
static void
__do_user_fault(struct task_struct *tsk, unsigned long addr,
		unsigned int fsr, unsigned int sig, int code,
		struct pt_regs *regs)

kernel空間

コンテキストが無かったり、ディスパッチ禁止(in_atomic()==true)の場合などに呼ばれます。ね。
/*
 * Oops.  The kernel tried to access some page that wasn't present.
 */
static void
__do_kernel_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
		  struct pt_regs *regs)
{
	/*
	 * Are we prepared to handle this kernel fault?
	 */
	if (fixup_exception(regs))
		return;

	/*
	 * No handler, we'll have to terminate things with extreme prejudice.
	 */
	bust_spinlocks(1);
	printk(KERN_ALERT
		"Unable to handle kernel %s at virtual address %08lx\n",
		(addr < PAGE_SIZE) ? "NULL pointer dereference" :
		"paging request", addr);

	show_pte(mm, addr);
	die("Oops", regs, fsr);
	bust_spinlocks(0);
	do_exit(SIGKILL);
}
カーネル空間でfaultが起きることは不具合以外にはレアケースだと考えられます。
基本的にカーネル自体はストレートマップされるので。ARMのようにメモリマップドIOで
ioremap()を忘れていたとか、範囲を間違ったとかならありえそうです。

あと、発生が予期できる個所に関しては、fixup_exception()で判定されます。
これは前回の記事へ。。。


ARMの資料をみる

Programmer's manualはぜひとも見ていただきたい。
Linux OSを意識した説明がしっかりと記述されていて、スタートアップコードや本記事の対応・実装説明についても記載があります.
■Cortex-A Series Programmer's manual
8.9.2 Emulation of dirty and accessed bits
Linux makes use of a bit associated with each page which marks whether the page is dirty (the page has been modified by the application and may therefore need to be written from memory to disk when the page is no longer needed).
This is not directly supported in hardware by ARM processors, and must therefore be implemented by kernel software.
When a page is first created, it is marked as read-only.
The first write to such a page (a clean page) will cause an MMU permission fault and the kernel data abort handler will be called.
The Linux memory management code will mark the page as dirty, if the page should indeed be writable, using the function handle_pte_fault() .
The page table entry is modified to make it allow both reads and writes and the TLB entry invalidated from the cache to ensure the MMU now uses the newly modified entry.
The abort handler then returns to re-try the faulting access in the application.
Linuxは、ページがダーティであるか否かをマークし、各ページに対応するビットを利用する。
ダーティである、とは、ページがアプリケーションによって変更されるか、そのページが必要ではなくなったときにメモリからディスクへ書き出す必要があることを示す。
ARMプロセッサでは、直接的にハードウェアでサポートされていない。したがってカーネルソフトウェアで実装される必要がある。
ページが最初に作られたとき、read-onlyとしてマークされる。
そのページ(クリーンなページ)に対して、最初の書き込みは、MMUのpermission faultの要因となり、カーネルのデータアボートハンドラが呼ばれる。
Linuxのメモリ管理コードは、そのページをdirtyと設定します。そのページが書き込み可能であるべきならば、handle_pte_fault()を使う。
ページテーブルエントリーは、読み書き可能に変更される。そして、TLBエントリは、MMUが今新しく変更されたエントリを使うため、キャッシュから無効化される。
その後、アボートハンドラは、アプリケーション内でfaultアクセスした命令を再実行するため、リターンする。
*訳注
handle_te_fault()は最終的によばれれるもので、PUD, PMDが無ければ取得するし、エラー判定になれば呼ばれず返る。

2014/10/08(水)procfsの辿り方

kernel coding:

http://www.linuxplanet.org/blogs/?cat=4558
http://tuxthink.blogspot.jp/2013/10/creating-read-write-proc-entry-in.html

proc_create("name", 0, NULL, &proc_fops);

Ex. fs/proc/cmdline.c
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>

static int cmdline_proc_show(struct seq_file *m, void *v)
{
        seq_printf(m, "%s\n", saved_command_line);
        return 0;
}

static int cmdline_proc_open(struct inode *inode, struct file *file)
{
        return single_open(file, cmdline_proc_show, NULL);
}

static const struct file_operations cmdline_proc_fops = {
        .open           = cmdline_proc_open,
        .read           = seq_read,
        .llseek         = seq_lseek,
        .release        = single_release,
};

static int __init proc_cmdline_init(void)
{
        proc_create("cmdline", 0, NULL, &cmdline_proc_fops);
        return 0;
}
module_init(proc_cmdline_init);
非常に判りよい例.. ;)

linux kernel debug(小技)

2014/07/25 linux::debug

kernel debug

driver毎のデバッグ出力

pr_debug()は"DEBUG"が定義されているときしか出力されない。
全域で表示させると、UBIなどの大量に吐き出してしまうモノがいるので厄介。
ファイルごとであれば、includeまえに
#define DEBUG (1)
とか、宣言を追加しても良いし、Makefileを編集しても良い。

ブロック単位?であれば、"FILE: driver/of/Makefile"に以下の一行を追加する。
ccflags-y := -DDEBUG
Flattened device tree関連のデバッグ出力が付与されて気持ちよく動作が見える。

参考資料

Documentation/kbuild/makefiles.txt
*1

*1 : このへんとか.. http://lxr.free-electrons.com/source/Documentation/kbuild/makefiles.txt

kexecの実装を調べる(ARM編)

2014/03/26 linux::ARM

kexecを追う

ユーザランドでは、kexec-toolsを用いる。仕組みがわかっていれば、
システムコールを自前で叩いて準備も出来そう。

実際の処理はアーキテクチャ依存部にて行われる。
ここではARMについて調査をすすめる。

FILE: kernel/kexec.c

SYSCALL_DEFINE4(kexec_load, unsigned long, entry, unsigned long, nr_segments,
		struct kexec_segment __user *, segments, unsigned long, flags)
{
...
			result = kimage_normal_alloc(&image, entry,
							nr_segments, segments);
...
		result = machine_kexec_prepare(image);
...
		for (i = 0; i < nr_segments; i++) {
			result = kimage_load_segment(image, &image->segment[i]);
...
		kimage_terminate(image);
kernel imageやinitrdのイメージを物理ページへ転送して起動準備。

load image用メモリ確保(kernel/kexec.c)

static int kimage_normal_alloc(struct kimage **rimage, unsigned long entry,
				unsigned long nr_segments,
				struct kexec_segment __user *segments)
{
...
	image->control_code_page = kimage_alloc_control_pages(image,
					   get_order(KEXEC_CONTROL_PAGE_SIZE));

image->control_code_page には、連続した4096page=16MBの物理連続空間が割り当てられる。
以下の順序で関数が呼ばれて取得できる。この領域はreboot用につかう。
struct page *kimage_alloc_control_pages(struct kimage *image,
					 unsigned int order)
{
	struct page *pages = NULL;

	switch (image->type) {
	case KEXEC_TYPE_DEFAULT:
		pages = kimage_alloc_normal_control_pages(image, order);
		break;
DEFAULTとCRASH時の2種類あり。今はDEFAULTを追う。
static struct page *kimage_alloc_normal_control_pages(struct kimage *image,
							unsigned int order)
{

		pages = kimage_alloc_pages(GFP_KERNEL, order);
...
		if ((epfn >= (KEXEC_CONTROL_MEMORY_LIMIT >> PAGE_SHIFT)) ||
			      kimage_is_destination_range(image, addr, eaddr)) {
			list_add(&pages->lru, &extra_pages);
			pages = NULL;
		}
	} while (!pages);
...
	kimage_free_page_list(&extra_pages);
物理的に連続するページを確保して、転送先アドレスとの重複チェックを行う。
該当する場合は改めて取得を試みる。最後に未使用分は開放する。



範囲チェック(FILE: arch/arm/kernel/machine_kexec.c)

int machine_kexec_prepare(struct kimage *image)
...
memblock_is_region_memory(current_segment->mem,current_segment->memsz)
現在のkernelが把握している memory空間の有効範囲が指示されているかをチェックしているようだ。

ロード

static int kimage_load_segment(struct kimage *image,
				struct kexec_segment *segment)
{
...
	case KEXEC_TYPE_DEFAULT:
		result = kimage_load_normal_segment(image, segment);

static int kimage_load_normal_segment(struct kimage *image,
					 struct kexec_segment *segment)
{


FILE:kernel/reboot.c

SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
		void __user *, arg)
{
#ifdef CONFIG_KEXEC
	case LINUX_REBOOT_CMD_KEXEC:
		ret = kernel_kexec();
kexecシステムコールで保持したイメージデータは、
rebootシステムコールのKEXEC要求により発動する。


FILE: kernel/kexec.c

/*
 * Move into place and start executing a preloaded standalone
 * executable.  If nothing was preloaded return an error.
 */
int kernel_kexec(void)
{
	{
		kexec_in_progress = true;
		kernel_restart_prepare(NULL);
		migrate_to_reboot_cpu();
		printk(KERN_EMERG "Starting new kernel\n");
		machine_shutdown();
	}

	machine_kexec(kexec_image);

ドライバ・ユーザヘルパ?の終了をしつつ(kernel_restart_prepare(NULL))、
CPU0のみ稼働状態(ほかは停止へ遷移させる)
FILE: kernel/reboot.c
void kernel_restart_prepare(char *cmd)
{
	blocking_notifier_call_chain(&reboot_notifier_list, SYS_RESTART, cmd);
	system_state = SYSTEM_RESTART;
	usermodehelper_disable();
	device_shutdown();
}


FILE: arch/arm/kernel/machine_kexec.c
void machine_kexec(struct kimage *image)
{
...
	unsigned long reboot_entry = (unsigned long)relocate_new_kernel;
...
	/* copy our kernel relocation code to the control code page */
	reboot_entry = fncpy(reboot_code_buffer,
			     reboot_entry,
			     relocate_new_kernel_size);
★imageで渡されたロードイメージをページ単位で保存し、
 そのPFNをLookUpTableとして管理している。
 そのポインタを受け取って、指定された物理アドレスへイメージを
 転送するアセンブラコード本体を、コレでコピーする。
 コードはentry pointへのjumpも含まれている。
...
	printk(KERN_INFO "Bye!\n");

	if (kexec_reinit)
		kexec_reinit();

	soft_restart(reboot_entry_phys);
}

FILE: arch/arm/kernel/relocate_kernel.S
ENTRY(relocate_new_kernel)
...
	/* Jump to relocated kernel */
	mov lr,r1
	mov r0,#0
	ldr r1,kexec_mach_type
	ldr r2,kexec_boot_atags
 ARM(	mov pc, lr	)
 THUMB(	bx lr		)

	.align

	.globl kexec_start_address
kexec_start_address:
	.long	0x0

	.globl kexec_indirection_page
kexec_indirection_page:
	.long	0x0

	.globl kexec_mach_type
kexec_mach_type:
	.long	0x0

	/* phy addr of the atags for the new kernel */
	.globl kexec_boot_atags
kexec_boot_atags:
	.long	0x0

ENDPROC(relocate_new_kernel)

FILE: arch/arm/kernel/process.c
void soft_restart(unsigned long addr)
{
	u64 *stack = soft_restart_stack + ARRAY_SIZE(soft_restart_stack);
	/* Disable interrupts first */
...
	/* Disable the L2 if we're the last man standing. */
...
	/* Change to the new stack and continue with the reset. */
	call_with_stack(__soft_restart, (void *)addr, (void *)stack);
FILE: arch/arm//lib/call_with_stack.S
ENTRY(call_with_stack)
	str	sp, [r2, #-4]!
	str	lr, [r2, #-4]!

	mov	sp, r2
	mov	r2, r0
	mov	r0, r1

	adr	lr, BSYM(1f)
	mov	pc, r2

1:	ldr	lr, [sp]
	ldr	sp, [sp, #4]
	mov	pc, lr
ENDPROC(call_with_stack)
ARM-EABI仕様にしたがい、r0にLUTへのポインタ(PFN/dest-adr/end-mark/next-LUTの入った先頭ページ)、r1に関数アドレス、r2にスタック(static変数)が渡されてくる。
ここで使われるスタック領域が気がかりであり、旧kernelのstatic変数領域なのだが、
ロードイメージがその領域を侵食しない保証がないのではなかろうか。
control_iamgeの後半から使うとか、予め1枚用意しておくなどしたほうが良い気はする。

まとめ

以上より、以下のことが解った。
  • 新しいイメージの保存先は、転送先アドレスにぶつからないように作られている。
  • 渡されたイメージは、任意の物理アドレスにコピーされ、entryへのjumpも行われる。\→kernel imageに限らず任意の領域に任意のデータをおけそう。
  • 最終的にcpu0/割り込み全部禁止状態で、entryへ飛ぶ
  • reboot前の処理として、なにか作りこみたい場合は、以下の関数ポインタへ適切な関数へのアドレスをセットする。\設定方法は、mach-*のあたりで、直接代入するしかなさそうだ(mach-kirkwoodのみ該当@3.14-rc8)。
    void (*kexec_reinit)(void);
    
kexecに失敗するようならば、以下の観点が必要。
cpu0以外が生きている状態で切り替えようとしていないか。
通常のpower on resetと同じ状態にするのが望ましい。
これはペリフェラルに関しても同じことが言える。power managementを有効化して、
ドライバはpower save有効と指示されれば、ペリフェラルを極力寝かせる。
状態としても、ほぼPOR直後と同じ状態にしておくことが望ましい。
ソフト的な状態保持は影響を受けないので、
制御レジスタ・外部デバイスの設定値が効いてくるだろう。

もしくは、初期化処理で、既に初期化された状態でも正常に
動作するような実装であれば良い。
OK キャンセル 確認 その他