Golangのfmtパッケージprintlnメソッドについて
Goなら分かるシステムプログラミングの最初の章を読み、printlnメソッドのデバッグをおこなった。
デバッグの結果をさらっと書き出して見た。
結論から言うと、printlnは最終的に「ファイルディスクリプタが1を表すファイルに対して、バイト列を書き込む命令をOSに対して行う関数である。」と言うことがわかった。以下からは順を追って説明して行く。
そこまで深く読み込んではいないのであしからず。流れは図のようになっていた。
fmt.printlnをデバッグしてみた
実際にデバッグして見たコードは以下のコードである。
package main
import (
"fmt"
)
func main() {
fmt.Println("hello wolrd!")
}
fmt.Printlnの部分にブレークポイントをうち、デバッグした。
Printlnメソッド
ステップインでfmt.Printlnメソッドにジャンプしてみる。すると以下のように定義されている。
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
printlnメソッドの引数は...interface型(interfaceのスライス型、今回はlen=1で"hello world!"が入っている。
関数の中身はFprintln(os.Stdout,a...)の呼び出しである。
よびだされたFprintlnメソッドは以下のような形式になっている。
func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintln(a)
n, err = w.Write(p.buf)
p.free()
return
}
Fprintlnメソッドの引き数のos.Stdoutとは
os.Stdoutはosパッケージfile.goで以下のように定義されている。
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
さらにNewFile関数の中をみてみる。
func NewFile(fd uintptr, name string) *File {
return newFile(fd, name, kindNewFile)
}
NewFile関数はnewFile関数を呼び出しており、newFile関数は構造体File型のポインタを返す関数である。以下のコードはnewFile関数である。読みやすいように省略している部分がある。
func newFile(fd uintptr, name string, kind newFileKind) *File {
fdi := int(fd)
if fdi < 0 {
return nil
}
f := &File{&file{
pfd: poll.FD{
Sysfd: fdi,
IsStream: true,
ZeroReadIsEOF: true,
},
name: name,
stdoutOrErr: fdi == 1 || fdi == 2,
}}
return f
}
ファイル型についてはあとで説明する。
次に引き数のsyscall.Stdoutについて見てみる。
syscall.Stdoutはsyscallパッケージsyscall_unix.goで定義されており、
var (
Stdin = 0
Stdout = 1
Stderr = 2
)
ここでは定数1が定義されている。
これがUNIX系のファイルディスクリプタである。
ファイルディスクリプタとは
OSは標準出力や標準入力をファイルへの読み書きや読み取りと同じように扱っている。
ファイルの読み書きや読み取りを行う際に重要なことがどのファイルに読み書きを行うかということである。つまり、ファイルを一意に表す識別値が必要になるのである。
この一意な識別値の事をファイルディスクリプタと呼ぶ。
ファイルディスクリプタはプロセス毎に付与され、標準入力、標準出力、標準エラー出力はUNIX系ではそれぞれ0,1,2と定義されている。それ以外のファイルはプロセス毎に3から付与されて行く。
つまり、今回のprintlnメソッドはファイルディスクリプタが1を表すファイル(標準出力)に書き込みを行うという命令をOSに対しておこなっているのである。
File型構造
File型自体の定義はosパッケージtypes.go
で定義されている
メソッドはfile.go
で書かれている。
type File struct {
*file // os specific
}
file型のポインタをフィールドにもつ構造体である。
file型とはなにか見てみる。file型はosパッケージのfile_unix.goで定義されている。
ファイル型はファイルの実態を表す構造体である。file型の先頭文字が小文字のfとなっており、osパッケージ内の閉じられた構造体である。ファイル利用時にはファイルの利用者がファイルの実態を定義することはできない。os.open関数を呼び出して、ファイルディスクリプタをOSに割り当ててもらってから、file型の実態が作られるのである。
pfdがファイルディスクリプタを表す。
type file struct {
pfd poll.FD
name string
dirinfo *dirInfo // nil unless directory being read
nonblock bool // whether we set nonblocking mode
stdoutOrErr bool // whether this is stdout or stderr
}
poll.FDにジャンプしてみる。
type FD struct {
// Lock sysfd and serialize access to Read and Write methods.
fdmu fdMutex
// System file descriptor. Immutable until Close.
Sysfd int
// I/O poller.
pd pollDesc
// Writev cache.
iovecs *[]syscall.Iovec
// Semaphore signaled when file is closed.
csema uint32
// Whether this is a streaming descriptor, as opposed to a
// packet-based descriptor like a UDP socket. Immutable.
IsStream bool
// Whether a zero byte read indicates EOF. This is false for a
// message based socket connection.
ZeroReadIsEOF bool
// Whether this is a file rather than a network socket.
isFile bool
// Whether this file has been set to blocking mode.
isBlocking bool
}
Sysfdというファイルディスクリプタを表すフィールドがある。
つまり先ほどのnewFile関数は
f := &File{&file{
pfd: poll.FD{
Sysfd: fdi,
IsStream: true,
ZeroReadIsEOF: true,
},
name: name,
stdoutOrErr: fdi == 1 || fdi == 2,
}}
はファイルディスクリプタ(pfd.Sysfd)が1を現すファイル型を作成する関数であったのである。
ここでFprintln関数に戻る。
Fprintlnメソッド
func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintln(a)
n, err = w.Write(p.buf)
p.free()
return
}
ここでみるのは引き数のio.Writer型である。これはインターフェース型である。
呼び出し時にはFprintln(os.Stdout, a...)
で呼び出していて、os.StdoutはFile型のポインタが戻り値である。File型はio.Writerインターフェースを満たす関数である。後に出てくるがWriteメソッドを実装している。
io.Writerインターフェース
io.Writerはioパッケージで定義されているインターフェースである。
ちなみにio.Writerインターフェースは以下のように定義されている。
type Writer interface {
Write(p []byte) (n int, err error)
}
これはバイト列pを受け取り、書きこんだバイト数nとエラーが起きたばあいにはそのエラーであるerrを返すというものである。
File型Writeメソッドからたどってみる
Fprintlnメソッド内の
n, err = w.Write(p.buf)
について見てみる。
それまでの
p := newPrinter()
p.doPrintln(a)
関数に関しては今回は見ないことにする。
きっとfmt.Printlnの引き数の文字列型をバイト型のスライスに変換していると推測する。
例えば"Hello World"を引き数で呼び出していたなら、
[]byte("Hello World")のような型変換が行われて、
値には文字列が文字コードの配列に変換されているはずである。
改めてn, err = w.Write(p.buf)
について見てみる。
Writeメソッドの定義元にジャンプするとosパッケージfile.go
内で以下のように定義されている。やはり、File型はio.Writerインターフェースを満たしていることが分かる。
func (f *File) Write(b []byte) (n int, err error) {
if err := f.checkValid("write"); err != nil {
return 0, err
}
n, e := f.write(b)
if n < 0 {
n = 0
}
if n != len(b) {
err = io.ErrShortWrite
}
epipecheck(f, e)
if e != nil {
err = f.wrapErr("write", e)
}
return n, err
ここでさらにn, e := f.write(b)
にジャンプしてみる。
これはosパッケージのfile_unix.goで定義されている。メソッド名が小文字なのでosパッケージないで閉じられたメソッドであることが分かる。
func (f *File) write(b []byte) (n int, err error) {
n, err = f.pfd.Write(b)
runtime.KeepAlive(f)
return n, err
}
n, err = f.pfd.Write(b)
を見てみる。ここではFile型のフィールドであるfile型のpfdフィールド(先ほど見た構造体FD型)のWriteメソッドが呼びだされている。
ここにジャンプしてみる。
以下のように定義されている。
func (fd *FD) Write(p []byte) (int, error) {
if err := fd.writeLock(); err != nil {
return 0, err
}
defer fd.writeUnlock()
if err := fd.pd.prepareWrite(fd.isFile); err != nil {
return 0, err
}
var nn int
for {
max := len(p)
if fd.IsStream && max-nn > maxRW {
max = nn + maxRW
}
n, err := syscall.Write(fd.Sysfd, p[nn:max])
if n > 0 {
nn += n
}
if nn == len(p) {
return nn, err
}
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitWrite(fd.isFile); err == nil {
continue
}
}
if err != nil {
return nn, err
}
if n == 0 {
return nn, io.ErrUnexpectedEOF
}
}
}
n, err := syscall.Write(fd.Sysfd, p[nn:max])
この部分でさらに書き込みを行う関数が呼び出されている。
ジャンプしてみる。
func Write(fd int, p []byte) (n int, err error) {
if race.Enabled {
race.ReleaseMerge(unsafe.Pointer(&ioSync))
}
n, err = write(fd, p)
if race.Enabled && n > 0 {
race.ReadRange(unsafe.Pointer(&p[0]), n)
}
if msanenabled && n > 0 {
msanRead(unsafe.Pointer(&p[0]), n)
}
return
}
このように定義されている。さらにwriteメソッドが呼びだされている。
ジャンプしてみる。
func write(fd int, p []byte) (n int, err error) {
var _p0 unsafe.Pointer
if len(p) > 0 {
_p0 = unsafe.Pointer(&p[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_WRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)))
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
ここでSyscall(SYS_WRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)))
出てくる。
ジャンプしてみる。ついにアセンブリ言語にたどり着く。
おそらく標準出力に対して文字列を書き込む命令が実装されているはずである。
流れ
- ファイルディスクリプタが標準出力を示すファイル型を作成する
- FileのWriteメソッドが呼ばれる
- Fileのwriteメソッドが呼ばれる
- fileの中のpfd(FD型)のWriteメソッドが呼ばれる
- syscallパッケージWirte()関数が呼び出される
- syscallパッケージwrite()関数が呼びだされる。
- アセンブリ言語でシステムコールが実装されている
まとめ
疲れた。
参考文献
- Goならわかるシステムプログラミング