2.3. 現在時刻の取得
2.3.1. RTC(Real Time Clock)の利用
x86版のxv6にはRTCから時刻情報を取得するための関数(cmostime())が用意されていますが、RISC-V版のxv6にはRTC関連のコードは用意されていません。
QEMUはRISC-V環境向けにもRTCを提供してくれているのでこれを利用して現在時刻を取得する機能を追加します。
Note
RISC-V版のxv6ではタイマー割り込みを用いて100ミリ秒毎にtickをカウントする機能だけが存在しています。
QEMUがRISC-V環境で提供しているRTCは「Goldfish RTC」です。これはGoogleがAndroidエミュレータ向けに開発したMMIOベースのRTCで、マッピングされたメモリアドレスへアクセスすることでRTCのレジスタを読み出すことができます。
RTCの物理アドレスの定義
まず、QEMUが提供するRTCの物理アドレスをkernel/memlayout.hに定義します。
📝 kernel/memlayout.h
// Physical memory layout
// qemu -machine virt is set up like this,
// based on qemu's hw/riscv/virt.c:
//
// 00001000 -- boot ROM, provided by qemu
+// 00101000 -- RTC
// 02000000 -- CLINT
// 0C000000 -- PLIC
// 10000000 -- uart0
// 10001000 -- virtio disk
// 80000000 -- qemu's boot ROM loads the kernel here,
// then jumps here.
// unused RAM after 80000000.
// the kernel uses physical memory thus:
// 80000000 -- entry.S, then kernel text and data
// end -- start of kernel page allocation area
// PHYSTOP -- end RAM used by the kernel
+// Goldfish RTC
+#define RTC 0x00101000L
+
// qemu puts UART registers here in physical memory.
#define UART0 0x10000000L
#define UART0_IRQ 10
...
Note
0x00101000Lという値はRTCを提供するQEMUの仮想ハードウェアの仕様で決められています。
物理アドレスを仮想アドレスにマッピング
kernel/vm.cにあるkvmmake()の中に、RTCの物理アドレスを仮想アドレスにマッピングするためのコードを追加します。
📝 kernel/vm.c
...
// Make a direct-map page table for the kernel.
pagetable_t
kvmmake(void)
{
pagetable_t kpgtbl;
kpgtbl = (pagetable_t)kalloc();
memset(kpgtbl, 0, PGSIZE);
+ // rtc registers
+ kvmmap(kpgtbl, RTC, RTC, PGSIZE, PTE_R | PTE_W);
+
// uart registers
kvmmap(kpgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);
...
}
...
Note
xv6カーネルではダイレクトマッピングが採用されており、マッピングする物理アドレスと仮想アドレスはどちらも同じ値となります。
RTCから時刻情報を読み出す関数
kernel/rtc.cを作成し、RTCから時刻情報を読み出す関数rtcread()を定義します。Goldfish RTCは8つの32bitレジスタを持ちますが、このうち時刻情報に関連するのはRTC_TIME_LOWとRTC_TIME_HIGHの2つです。この2つのレジスタに、64bitの時刻情報を32bitづつ格納しています。リトルエンディアンの環境であればRTC_TIME_LOWからまとめて64bit読み出すことでRTCが保持している時刻情報をそのまま取得できます。
Note
64bitの時刻情報はUNIXエポック(1970年1月1日午前0時0分0秒 UTC)からの経過ナノ秒の値となっています。
📝 kernel/rtc.c
#include "types.h"
#include "riscv.h"
#include "memlayout.h"
#include "defs.h"
#define RTC_TIME_LOW 0x00
#define RTC_TIME_HIGH 0x04
uint64
rtcread(void)
{
return *(volatile uint64 *)(RTC + RTC_TIME_LOW);
}
プロトタイプ宣言の追加
追加した関数のプロトタイプ宣言をkernel/defs.hに追加します。
📝 kernel/defs.h
...
int either_copyout(int user_dst, uint64 dst, void *src, uint64 len);
int either_copyin(void *dst, int user_src, uint64 src, uint64 len);
void procdump(void);
+// rtc.c
+uint64 rtcread(void);
// swtch.S
void swtch(struct context*, struct context*);
...
Makefileの修正
新しくソースファイルを追加したので、オブジェクトファイルのリスト(OBJS)に定義を追加します。
📝 Makefile
K=kernel
U=user
OBJS = \
...
$K/pipe.o \
$K/exec.o \
$K/sysfile.o \
$K/kernelvec.o \
$K/plic.o \
+ $K/rtc.o \
$K/virtio_disk.o
...
動作確認
kernel/main.cのmain()に動作確認用のコードを追加します。
rtcread()を呼び出して64bitの時刻情報を取得- 64bitの時刻情報を「秒」と「ナノ秒」に分けて出力
📝 kernel/main.c
...
// start() jumps here in supervisor mode on all CPUs.
void
main()
{
if (cpuid() == 0) {
consoleinit();
printkinit();
printk("\n");
printk("xv6 kernel is booting\n");
printk("\n");
kinit(); // physical page allocator
kvminit(); // create kernel page table
kvminithart(); // turn on paging
procinit(); // process table
trapinit(); // trap vectors
trapinithart(); // install kernel trap vector
plicinit(); // set up interrupt controller
plicinithart(); // ask PLIC for device interrupts
binit(); // buffer cache
iinit(); // inode table
fileinit(); // file table
virtio_disk_init(); // emulated hard disk
+ uint64 rtc = rtcread();
+ printk("%ld.%09ld\n", rtc / 1000000000, rtc % 1000000000);
userinit(); // first user process
__atomic_store_n(&started, 1, __ATOMIC_RELEASE);
} else {
while (__atomic_load_n(&started, __ATOMIC_ACQUIRE) == 0)
;
printk("hart %d starting\n", cpuid());
kvminithart(); // turn on paging
trapinithart(); // install kernel trap vector
plicinithart(); // ask PLIC for device interrupts
}
scheduler();
}
再ビルドした後、make qemuを実行してxv6を起動させます。
xv6 kernel is booting
1786522635.308689000
hart 1 starting
hart 2 starting
init: starting sh
$
シェルが立ち上がる前の起動ログの中に「秒.ナノ秒」の形式で時刻情報が出力されます。このうち秒の部分はUNIXタイムそのものです。
Tip
dateコマンドを利用するとUNIXタイムを任意の書式の時刻に変換できます。これを利用して出力されているUNIXタイムが正しい値かどうか検証してみましょう。開発環境のシェルで次のコマンドを実行してください。$ date -d @1786522635 +"%Y/%m/%d %T"
2.3.2. 現在時刻を得る関数の追加
rtcread()でRTCから現在時刻を取得できるようになったので、これをベースにして現在時刻を得るために使われているtime()とgettimeofday()を作成します。
型定義の追加
kernel/types.hに、time()が使用するtime_tの定義を追加します。
📝 kernel/types.h
typedef unsigned int uint;
typedef unsigned short ushort;
typedef unsigned char uchar;
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;
typedef unsigned long uint64;
typedef uint64 pde_t;
+typedef long time_t;
+
#if defined(_STDIO_H)
#define MKFS
#endif
...
構造体定義の追加
新しくkernel/time.hを作成し、gettimeofday()が使用するstruct timevalを定義します。
📝 kernel/time.h
struct timeval {
long tv_sec;
long tv_usec;
};
関数の追加
新しくkernel/time.cを作成し、time()とgettimeofday()関数を定義します。
📝 kernel/time.c
#include "types.h"
#include "riscv.h"
#include "defs.h"
#include "time.h"
time_t
time(time_t *t)
{
time_t _t;
if (!t)
t = &_t;
*t = rtcread() / 1000000000;
return *t;
}
int
gettimeofday(struct timeval *tv, void *tz)
{
(void)tz;
uint64 rtc = rtcread();
tv->tv_sec = rtc / 1000000000;
tv->tv_usec = (rtc % 1000000000) / 1000;
return 0;
}
プロトタイプ宣言の追加
追加した関数のプロトタイプ宣言をkernel/defs.hに追加します。
📝 kernel/defs.h
struct buf;
struct context;
struct file;
struct inode;
struct pipe;
struct proc;
struct spinlock;
struct sleeplock;
struct stat;
struct superblock;
+struct timeval;
...
// syscall.c
void argint(int, int*);
int argstr(int, char*, int);
void argaddr(int, uint64 *);
int fetchstr(uint64, char*, int);
int fetchaddr(uint64, uint64*);
void syscall();
+// time.c
+time_t time(time_t*);
+int gettimeofday(struct timeval*, void*);
// trap.c
extern uint ticks;
void trapinit(void);
void trapinithart(void);
extern struct spinlock tickslock;
void prepare_return(void);
...
Makefileの修正
新しくソースファイルを追加したので、オブジェクトファイルのリスト(OBJS)に定義を追加します。
📝 Makefile
K=kernel
U=user
OBJS = \
...
$K/sysfile.o \
$K/kernelvec.o \
$K/plic.o \
$K/rtc.o \
+ $K/time.o \
$K/virtio_disk.o
...
動作確認
kernel/main.cのmain()にある動作確認用のコードを書き換えます。
Warning
gettimeofday()を使用する際はtime.hが必要なのでインクルードを忘れないようにしてください。
📝 kernel/main.c
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "defs.h"
+#include "time.h"
volatile static int started = 0;
// start() jumps here in supervisor mode on all CPUs.
void
main()
{
...
- uint64 rtc = rtcread();
- printk("%ld.%09ld\n", rtc / 1000000000, rtc % 1000000000);
+ struct timeval tv;
+ gettimeofday(&tv, NULL);
+ printk("tv: {sec: %ld, usec: %ld}\n", tv.tv_sec, tv.tv_usec);
...
}
再ビルドした後、make qemuを実行してxv6を起動させます。
xv6 kernel is booting
tv: {sec: 1786522939, usec: 308376}
hart 2 starting
hart 1 starting
init: starting sh
$
struct timeval型の変数が保持している秒とマイクロ秒が出力されます。
Note
ナノ秒の精度が必要な場合は
struct timespecを返すclock_gettime()を作成するといいでしょう。
2.3.3. カレンダー形式への変換
UNIXタイムのままだと可読性が乏しいため、カレンダー形式の日時情報を保持するstruct tmを追加します。加えて、UNIXタイムとカレンダー形式の値を相互に変換するための関数を作ります。
構造体定義の追加
カレンダー形式の日時情報を扱うための構造体(struct tm)の定義をkernel/time.hに追加します。
📝 kernel/time.h
struct timeval {
long tv_sec;
long tv_usec;
};
+
+struct tm {
+ int tm_sec; // 0-60
+ int tm_min; // 0-59
+ int tm_hour; // 0-23
+ int tm_mday; // 1-31
+ int tm_mon; // 0-11
+ int tm_year; // since 1900
+ int tm_wday; // 0-6
+ int tm_yday; // 0-365
+ int tm_isdst; // zero
+};
Important
struct tmを扱う際には以下の点に注意しましょう。
tm_mday(日)だけ1から始まるtm_year(年)は1900年からの経過年数tm_wday(曜日)は日曜日(0)から土曜日(6)までの値tm_isdstはサマータイムに関する値(ここでは常にゼロにしておく)
関数の追加
ここでは、kernel/time.cへ以下の4つの関数を追加します。
isleapyear(): うるう年を判定するための関数(内部利用のみ)ndays(): 指定した年月に存在する日数を得るための関数(内部利用のみ)mktime(): カレンダー形式の値(struct tm)からUNIXタイム(time_t)へ変換する関数localtime_r(): UNIXタイム(time_t)からカレンダー形式の値(struct tm)へ変換する関数
📝 kernel/time.c
#include "types.h"
#include "riscv.h"
#include "defs.h"
#include "time.h"
+
+#define TZ_OFFSET 9 //JST
...
int
gettimeofday(struct timeval *tv, void *tz)
{
(void)tz;
uint64 rtc = rtcread();
tv->tv_sec = rtc / 1000000000;
tv->tv_usec = (rtc % 1000000000) / 1000;
return 0;
}
+static int
+isleapyear(int y)
+{
+ return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
+}
+
+static int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+
+static int
+ndays(int y, int m)
+{
+ int n = days[m];
+
+ if (m == 1 && isleapyear(y)) {
+ n++;
+ }
+ return n;
+}
+
+time_t
+mktime(struct tm *tm)
+{
+ const int epoch = 1970;
+ time_t result = 0;
+
+ for (int y = epoch; y < tm->tm_year + 1900; y++) {
+ result += (isleapyear(y) ? 366 : 365) * 24 * 3600;
+ }
+ for (int m = 0; m < tm->tm_mon; m++) {
+ result += ndays(tm->tm_year + 1900, m) * 24 * 3600;
+ }
+ result += (tm->tm_mday - 1) * 24 * 3600;
+ result += tm->tm_hour * 3600;
+ result += tm->tm_min * 60;
+ result += tm->tm_sec;
+ result -= TZ_OFFSET * 3600;
+ return result;
+}
+
+struct tm *
+localtime_r(const time_t *timep, struct tm *result)
+{
+ time_t local_time;
+
+ local_time = *timep + (TZ_OFFSET * 3600);
+ result->tm_sec = local_time % 60;
+ local_time /= 60;
+ result->tm_min = local_time % 60;
+ local_time /= 60;
+ result->tm_hour = local_time % 24;
+ local_time /= 24;
+
+ int days = local_time;
+ result->tm_wday = (days + 4) % 7;
+
+ int y = 1970;
+ while (1) {
+ int n = isleapyear(y) ? 366 : 365;
+ if (days < n) {
+ break;
+ }
+ days -= n;
+ y++;
+ }
+ result->tm_year = y - 1900;
+ result->tm_yday = days;
+
+ int m = 0;
+ while (1) {
+ int n = ndays(y, m);
+ if (days < n) {
+ break;
+ }
+ days -= n;
+ m++;
+ }
+ result->tm_mon = m;
+ result->tm_mday = days + 1;
+ result->tm_isdst = 0;
+ return result;
+}
Warning
簡略化のためにカレンダー形式では固定的に日本のタイムゾーン(JST)に合わせた値を保持するようにしてます。
プロトタイプ宣言の追加
追加した関数のプロトタイプ宣言をkernel/defs.hに追加します。
📝 kernel/defs.h
struct buf;
struct context;
struct file;
struct inode;
struct pipe;
struct proc;
struct spinlock;
struct sleeplock;
struct stat;
struct superblock;
struct timeval;
+struct tm;
...
// time.c
time_t time(time_t*);
int gettimeofday(struct timeval*, void*);
+time_t mktime(struct tm*);
+struct tm* localtime_r(const time_t*, struct tm*);
...
動作確認
kernel/main.cのmain()に動作確認用のコードを追加します。
📝 kernel/main.c
...
struct timeval tv;
gettimeofday(&tv, NULL);
printk("tv: {sec: %ld, usec: %ld}\n", tv.tv_sec, tv.tv_usec);
+ struct tm tm;
+ localtime_r(&tv.tv_sec, &tm);
+ printk("%04d/%02d/%02d %02d:%02d:%02d\n",
+ tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
+ printk("%ld\n", mktime(&tm));
...
再ビルドした後、make qemuを実行してxv6を起動させます。
xv6 kernel is booting
tv: {sec: 1786523184, usec: 308273}
2026/08/12 17:26:24
1786523184
hart 1 starting
hart 2 starting
init: starting sh
$
struct timevalの値に続けて、localtime_r()で取得したstruct tmの値を用いたカレンダー形式の日時情報と、mktime()でstruct tmから逆変換したUNIXタイムが出力されるはずです。
動作確認用コードの整理
動作確認が済んだら、main()に直接書いていた一時的なコードを整理します。カレンダー形式の日時表示は起動メッセージとして残しておくと便利なので、printdate()という関数にまとめます。それ以外の確認用の出力(struct timevalの生の値とmktime()による逆変換)は削除します。
📝 kernel/main.c
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "defs.h"
#include "time.h"
volatile static int started = 0;
+static void
+printdate()
+{
+ struct timeval tv;
+ struct tm tm;
+ gettimeofday(&tv, NULL);
+ localtime_r(&tv.tv_sec, &tm);
+ printk("%04d/%02d/%02d %02d:%02d:%02d\n",
+ tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
+}
+
// start() jumps here in supervisor mode on all CPUs.
...
virtio_disk_init(); // emulated hard disk
- struct timeval tv;
- gettimeofday(&tv, NULL);
- printk("tv: {sec: %ld, usec: %ld}\n", tv.tv_sec, tv.tv_usec);
- struct tm tm;
- localtime_r(&tv.tv_sec, &tm);
- printk("%04d/%02d/%02d %02d:%02d:%02d\n",
- tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
- printk("%ld\n", mktime(&tm));
+ printdate();
userinit(); // first user process
...
再ビルドして起動すると、起動メッセージに日時だけが表示されます。以降の章はこの状態を前提に進めます。
xv6 kernel is booting
2026/08/12 17:26:24
hart 1 starting
hart 2 starting
init: starting sh
$