Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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_LOWRTC_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.cmain()に動作確認用のコードを追加します。

  • 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.cmain()にある動作確認用のコードを書き換えます。

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.cmain()に動作確認用のコードを追加します。

📝 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
$