UNIX 7th code reading - ファイルの読み書き処理
はじめに
今回はファイルの読み書き処理を見ていきます。クローズ処理は次回に回します。
read( ), write( )
- read( )
- write( )
ユーザプログラムからsys read(ファイルの読み込み), sys write(ファイルの書き込み)が実行されると、それぞれカーネルのread( )とwrite( )が実行されます。
readはFREAD, writeはFWRITEフラグを引数にrdwr( )を呼び出します。6thと同じ処理です。
rdwr( )
http://www.tamacom.com/tour/kernel/unix/S/99.html#L30
rdwr( )はファイル読み書きのためにu.u_base, u.u_count, u.u_offset, u.u_segflgをセットしてからreadi( ), writei( )を呼び出します。パイプの読み書きをする場合はreadi( ), writei( )ではなくreadp( ), writep( )を呼び出します。
7thから追加された論理が二箇所有ります。
58 ip = fp->f_inode; 59 if (fp->f_flag&FMP) 60 u.u_offset = 0; 61 else 62 u.u_offset = fp->f_un.f_offset; 63 if((ip->i_mode&(IFCHR&IFBLK)) == 0) 64 plock(ip); 65 if(mode == FREAD) 66 readi(ip); 67 else 68 writei(ip); 69 if((ip->i_mode&(IFCHR&IFBLK)) == 0) 70 prele(ip); 71 if ((fp->f_flag&FMP) == 0) 72 fp->f_un.f_offset += uap->count-u.u_count;
対象inodeが特殊型でなかった場合、readi( ), writei( )の前後でinodeに対しplock( ), prele( )によって排他処理が行われるようになりました。
疑問1. 上記63, 69行目はIFCHR & IFBLKではなくてIFCHR | IFBLKではないだろうか?
疑問2. 6thではこの箇所に排他処理が入っていなかった。複数プロセスが同時に同じinodeに対する読み書き処理をし得た?
また、ファイルにFMPフラグが立っていたらオフセットを0(先頭から読み書き)にし、また読み書き処理後にファイルのオフセットを更新しないようになりました。このFMPというフラグは7thからついかされたもので、どうやらMPX(MultiPleXed) filesというものに使われるようなのですが、その詳細はまだ理解できていません。解明するためにMPX(2)を読んでいるところです。
readi( )
続いてrdwr( )から呼ばれるreadi( )を見ていきます。
http://www.tamacom.com/tour/kernel/unix/S/94.html#L19
readi( )はbmap( )関数を使って、読み出し対象のデータブロックNoを取得し、bread( )、もしくはbread( )を使ってファイルの読み出し処理を開始します。読み出し処理完了後にiomove( )を使ってユーザ領域に読み出したデータをコピーします。
31 if(u.u_offset < 0) { 32 u.u_error = EINVAL; 33 return; 34 }
今回からu.u_offsetが0未満でないかのチェックが入り、妥当性検証の強化されたようです。u.u_offsetが0未満になることはあるのでしょうか? オーバーフロー対策?
58 if ((long)bn<0) { 59 bp = geteblk(); 60 clrbuf(bp); 61 } else if (ip->i_un.i_lastr+1==lbn) 62 bp = breada(dev, bn, rablock); 63 else 64 bp = bread(dev, bn);
また、longでキャストしたbn(通常ファイルの場合はbmap( )の結果)が0未満の場合は、ファイルの読み出し処理を行わずに、getblk( )でバッファを取得し、clrbuf( )でそのバッファをクリアしています。
疑問3. bnが0になるのはどういうとき? エラー? → 追記:bmap( )でエラーが起きた場合-1が返る。詳しくは↓↓に書いたbmap( )の解説を参照のこと。
疑問4. バッファを取得・クリアしているのは何のため? 後の処理を通常読み出し時と合わせるために、(クリアされた)バッファを必要としている?
writei( )
続いてreadi( )と同様にrdwr( )から呼ばれるwritei( )を見ていきます。
http://www.tamacom.com/tour/kernel/unix/S/94.html#L83
writei( )はbmap( )関数を使って、読み出し対象のデータブロックNoを取得し、iomove( )を使ってユーザ領域からバッファにデータをコピーした後に、bdwrite( )を使ってバッファのデータをファイルに書き込みます。
92 if(u.u_offset < 0) { 93 u.u_error = EINVAL; 94 return; 95 }
readi( )と同様にu.u_offsetのチェックが加わっています。
121 if(u.u_error != 0) 122 brelse(bp); 123 else 124 bdwrite(bp);
また、ファイルへの書き込みを行うときは、必ずbdwrite( )(遅延書き込み)を使うようになりました。6thでは、オフセットがブロックサイズ(512Bytes)で切れる場合は、bawrite( )で非同期的に"直ちに"ファイルへ書き込んでいました。
6thと比較すると、7thの方がブロックデバイスへのアクセスが減ると思われます。例えば、書き込んだデータを(遅延書き込み処理が行われる前に)すぐに読み出し、更新して再度書き込む場合を考えます。6thの場合には2回ブロックデバイスへのアクセスが発生するのですが、7thの場合は最初の書き込み処理時にはブロックデバイスへのアクセスが発生しません。
疑問5. 6thはなぜ512bytesで切れる場合にbawrite( )を使用していた?
128 ip->i_flag |= IUPD|ICHG;
書き込み処理が完了した後に、inodeにIUPDだけでなくICHGフラグも立つようになりました。ICHGフラグは7thから追加されたものです。
http://www.tamacom.com/tour/kernel/unix/S/61.html#L54
53 /* flags */ 54 #define ILOCK 01 /* inode is locked */ 55 #define IUPD 02 /* file has been modified */ 56 #define IACC 04 /* inode access time to be updated */ 57 #define IMOUNT 010 /* inode is mounted on */ 58 #define IWANT 020 /* some process waiting on lock */ 59 #define ITEXT 040 /* inode is pure text prototype */ 60 #define ICHG 0100 /* inode has been changed */
inode.hを見るとわかるように、7thからファイルが更新された(=IUPD)ことと、inodeが更新された(=ICHG)ことを区別して管理するようになったようです。
bmap( )
続いてinodeとデータブロックのマッピングを行うbmap( )を見ていきます。
http://www.tamacom.com/tour/kernel/unix/S/97.html#L18
7thのデータブロックのマッピングの絵を再掲しておきます。
データブロックへのマッピングの仕様が6thから変わったので、当然ソースコードもがらっと変わっています。簡単に解説していくと
17 daddr_t 18 bmap(ip, bn, rwflg) 19 register struct inode *ip; 20 daddr_t bn; 21 { 22 register i; 23 struct buf *bp, *nbp; 24 int j, sh; 25 daddr_t nb, *bap; 26 dev_t dev; 27 28 if(bn < 0) { 29 u.u_error = EFBIG; 30 return((daddr_t)0); 31 } 32 dev = ip->i_dev; 33 rablock = 0; 34
引数は以下
- ip : inode
- bn : 論理的なblock No. ファイル中の何ブロック目かを表す。オフセットから512(=1blockのサイズ)を割ることで算出される
- rwflg : 読み書きどちらを行おうとしているか
bnが負でないことをチェックし、メインロジックへ突入します。
35 /* 36 * blocks 0..NADDR-4 are direct blocks 37 */ 38 if(bn < NADDR-3) { 39 i = bn; 40 nb = ip->i_un.i_addr[i]; 41 if(nb == 0) { 42 if(rwflg==B_READ || (bp = alloc(dev))==NULL) 43 return((daddr_t)-1); 44 nb = bp->b_blkno; 45 bdwrite(bp); 46 ip->i_un.i_addr[i] = nb; 47 ip->i_flag |= IUPD|ICHG; 48 } 49 if(i < NADDR-4) 50 rablock = ip->i_un.i_addr[i+1]; 51 return(nb); 52 } 53
最初に該当ブロックが直接参照ブロックの場合だったときの処理を行います。
該当するinodeのi_addr[ ]が0だった場合、読み出し処理時の場合は-1(エラー)を返します。読もうとしているデータブロックがまだ存在していないからです。ファイルサイズより大きいアドレスを読もうとするときなどに起こりうるでしょう。
書き込み処理時の場合には、新たにデータブロックを確保するためにalloc( )を実行します。この段階でbdwrite( )を呼んでいるのは、B_DONEフラグを立てる(キャッシュを有効にする)ためではないかと思われます。
該当データブロックが直接参照ブロックの最後のブロックでなかった場合、次のデータブロックNoを先読みデータブロックNoを示すrablockに格納しておきます。
直接参照ブロックと間接参照ブロックをまたがって処理を行おうとする場合、その切れ目のタイミングでは先読みが行われないようです。
最後に物理的なデータブロックNoをreturnします。
54 /* 55 * addresses NADDR-3, NADDR-2, and NADDR-1 56 * have single, double, triple indirect blocks. 57 * the first step is to determine 58 * how many levels of indirection. 59 */ 60 sh = 0; 61 nb = 1; 62 bn -= NADDR-3; 63 for(j=3; j>0; j--) { 64 sh += NSHIFT; 65 nb <<= NSHIFT; 66 if(bn < nb) 67 break; 68 bn -= nb; 69 } 70 if(j == 0) { 71 u.u_error = EFBIG; 72 return((daddr_t)0); 73 } 74 75 /* 76 * fetch the address from the inode 77 */ 78 nb = ip->i_un.i_addr[NADDR-j]; 79 if(nb == 0) { 80 if(rwflg==B_READ || (bp = alloc(dev))==NULL) 81 return((daddr_t)-1); 82 nb = bp->b_blkno; 83 bdwrite(bp); 84 ip->i_un.i_addr[NADDR-j] = nb; 85 ip->i_flag |= IUPD|ICHG; 86 } 87 88 /* 89 * fetch through the indirect blocks 90 */ 91 for(; j<=3; j++) { 92 bp = bread(dev, nb); 93 if(bp->b_flags & B_ERROR) { 94 brelse(bp); 95 return((daddr_t)0); 96 } 97 bap = bp->b_un.b_daddr; 98 sh -= NSHIFT; 99 i = (bn>>sh) & NMASK; 100 nb = bap[i]; 101 if(nb == 0) { 102 if(rwflg==B_READ || (nbp = alloc(dev))==NULL) { 103 brelse(bp); 104 return((daddr_t)-1); 105 } 106 nb = nbp->b_blkno; 107 bdwrite(nbp); 108 bap[i] = nb; 109 bdwrite(bp); 110 } else 111 brelse(bp); 112 } 113 114 /* 115 * calculate read-ahead. 116 */ 117 if(i < NINDIR-1) 118 rablock = bap[i+1]; 119 return(nb); 120 }
続いて、間接参照ブロックの処理を行います。
データブロックを何度か辿って、該当のデータブロックNoを割り出します。
直接参照ブロックの処理と7thのデータブロックマッピングの仕様と6thのbmap( )の解説を見れば処理内容はわかるかと思いますので、詳細な解説は省略します。
(6thではえらくわかりづらいコードだったのですが、7thではかなり読みやすいものになりましたし)
iomove( )
最後にreadi( ), writei( )から呼び出されるiomove( )を見ていきます。
http://www.tamacom.com/tour/kernel/unix/S/94.html#L173
iomove( )はバッファのデータとユーザ領域のデータをやり取りする関数です。
181 if(u.u_segflg != 1 && 182 (n&(NBPW-1)) == 0 && 183 ((int)cp&(NBPW-1)) == 0 && 184 ((int)u.u_base&(NBPW-1)) == 0) { 185 if (flag==B_WRITE) 186 if (u.u_segflg==0) 187 t = copyin(u.u_base, (caddr_t)cp, n); 188 else 189 t = copyiin(u.u_base, (caddr_t)cp, n); 190 else 191 if (u.u_segflg==0) 192 t = copyout((caddr_t)cp, u.u_base, n); 193 else 194 t = copyiout((caddr_t)cp, u.u_base, n); 195 if (t) { 196 u.u_error = EFAULT; 197 return; 198 } 199 u.u_base += n; 200 u.u_offset += n; 201 u.u_count -= n; 202 return; 203 }
u.u_segflgが0でも1でもないときは、copyiin( ), copyiout( )という関数が使用されるようです。
u.u_segflgが0, 1以外に設定されるのはxalloc( )のみのようです。
http://www.tamacom.com/tour/kernel/unix/S/103.html#L133
130 u.u_count = u.u_exdata.ux_tsize; 131 u.u_offset = sizeof(u.u_exdata); 132 u.u_base = 0; 133 u.u_segflg = 2; 134 u.u_procp->p_flag |= SLOCK; 135 readi(ip);
135行目のreadi( )のときにのみcopyiout( )が使用されるようです。
疑問6. copyiin( )はいつ使われる?
疑問7. copyiin( ), copyiout( )はcopyin( ), copyout( )と何が違う?
疑問7.についてですが、これらの関数はアセンブラで書かれているため、調査は後回しにします。xalloc( )から呼ばれるので、共有テキストセグメント絡みな話な気がしますが。
終わりに
疑問点がいくつか残ったままですが、折を見て真面目に調査をしようと思います。それまでは、とりあえずどんどん先に進んでいきます。
次回こそはファイルのクローズ処理を見ていく予定です。