ioniceの「none: prio 4」の意味を掘り下げる

# いきなり結論
- Q. ioniceの結果のこの2つて違うの？「**none: prio 4**」「**none: prio 0**」
- A. 同じです。kernelマニア以外はそっとページを閉じてもらって問題ないです。

# はじめに
Linuxで**ionice(1)**しまくると、noneなのにprioの値が2種類あることに気づく。

```bash
[rarul@tina ~]$ cd /proc
[rarul@tina proc]$ ionice -p *[0-9]* |sort |uniq
best-effort: prio 3
none: prio 0
none: prio 4
```

**none**は[先の記事の通り](http://qiita.com/rarul/items/3d80436a9fea90445cdb)**IOPRIO_CLASS_NONE**であることを表す。IOPRIO_CLASS_NONEは何も設定していないときのデフォルトのクラス。ただ、[util-linux/schedutils/ionice.c](https://github.com/karelzak/util-linux/blob/v2.29.2/schedutils/ionice.c#L210)や[kernel/block/ioprio.c](http://lxr.free-electrons.com/source/block/ioprio.c?v4.10#L61)より、IOPRIO_CLASS_NONEの時はpriorityは0しか設定できない。

```c]ionice.c
		case IOPRIO_CLASS_NONE:
			if ((set & 1) && !tolerant)
				warnx(_("ignoring given class data for none class"));
			data = 0;
			break;
```

```c:ioprio.c
 61 SYSCALL_DEFINE3(ioprio_set, int, which, int, who, int, ioprio)
 62 {
 63         int class = IOPRIO_PRIO_CLASS(ioprio);
 64         int data = IOPRIO_PRIO_DATA(ioprio);
(----------snip----------)
 83                 case IOPRIO_CLASS_NONE:
 84                         if (data)
 85                                 return -EINVAL;
 86                         break;
```

じゃioniceが表示する「none: prio 4」っていったいなんなんだ？という疑問がわいてくる。これを読み解くという趣旨の雑記です。なお、Linux-4.10くらいとutil-linux-2.29.2くらいを見ています。

# いつこうなるのか「none: prio 4」
いつ「none: prio 4」になるのかはものすごく単純で、[kernel/ioprio.cの**get_task_ioprio()**](http://lxr.free-electrons.com/source/block/ioprio.c?v=4.10#L145)を見ればわかる。

```c:ioprio.c
145 static int get_task_ioprio(struct task_struct *p)
146 {
147         int ret;
148 
149         ret = security_task_getioprio(p);
150         if (ret)
151                 goto out;
152         ret = IOPRIO_PRIO_VALUE(IOPRIO_CLASS_NONE, IOPRIO_NORM);
153         task_lock(p);
154         if (p->io_context)
155                 ret = p->io_context->ioprio;
156         task_unlock(p);
157 out:
158         return ret;
159 }
```

**p->io_context**がNULLなら**IOPRIO_PRIO_VALUE(IOPRIO_CLASS_NONE, IOPRIO_NORM)**が[**ioprio_get(2)**](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/ioprio_set.2.html)の値として返ってくる。IOPRIO_NORMは[kernel/include/linux/ioprio.h](http://lxr.free-electrons.com/source/include/linux/ioprio.h?v=4.10#L43)より、

```c:ioprio.h
 43 /*
 44  * Fallback BE priority
 45  */
 46 #define IOPRIO_NORM     (4)
```

となる。つまり、io_contextがNULLの時になぜか、**0**ではなく、IOPRIO_CLASS_BEのデフォルト値である**IOPRIO_NORM**を選んでいる。どう見てもただのバグです。本当にありがとうございました。

・・・いや、io_contextがNULLってどういうこと？そもそもいつインスタンスが入るの？

# io_contextが初期化されるタイミング
**CLONE_IO**を使って共用する特殊ケースを除くと、io_contextを初期化する関数は[kernel/block/blk-ioc.c:**create_task_io_context()**](http://lxr.free-electrons.com/source/block/blk-ioc.c?v=4.10#L260)である。

```c:blk-ioc.c
260         task_lock(task);
261         if (!task->io_context &&
262             (task == current || !(task->flags & PF_EXITING)))
263                 task->io_context = ioc;
264         else
265                 kmem_cache_free(iocontext_cachep, ioc);
266 
267         ret = task->io_context ? 0 : -EBUSY;
268 
269         task_unlock(task);
```

create_task_io_context()を呼ぶ系は、大きく3つある。
### 1. requestを作るとき
ioを動かすためにrequestを作るときにcreate_io_context()からcreate_task_io_context()を呼ぶ。blk-core.cあたり。ただし遅延writeはkworkerやjbd2/sda1のcontextで行われるので、来るのは、readやO_SYNCつきwriteや明示的にsync(2)した場合などとなる。このときおおむね下記のようなbacktraceになる。

1. create_task_io_context
1. generic_make_request_checks
1. generic_make_request
1. submit_bio
1. mpage_bio_submit
1. mpage_readpages
1. blkdev_readpages
1. \_\_do_page_cache_readahead
1. force_page_cache_readahead
1. page_cache_sync_readahead
1. generic_file_read_iter
1. blkdev_read_iter
1. new_sync_read
1. vfs_read
1. SyS_read

### 2. ioprio_set()するとき
ioprio_set(2)すると、set_task_ioprio()->get_task_io_context()->create_task_io_context()と呼ばれる。このときioprio_setしたいioprio値に設定される。

### 3. clone()するとき
copy_io()->get_task_io_context()->create_task_io_context()と呼ばれる。[kernel/kernel/fork.c:**copy_io()**](http://lxr.free-electrons.com/source/kernel/fork.c?v=4.10#L1251)より、


```c:fork.c
1251 static int copy_io(unsigned long clone_flags, struct task_struct *tsk)
1252 {
1253 #ifdef CONFIG_BLOCK
1254         struct io_context *ioc = current->io_context;
1255         struct io_context *new_ioc;
1256 
1257         if (!ioc)
1258                 return 0;
1259         /*
1260          * Share io context with parent, if CLONE_IO is set
1261          */
1262         if (clone_flags & CLONE_IO) {
1263                 ioc_task_link(ioc);
1264                 tsk->io_context = ioc;
1265         } else if (ioprio_valid(ioc->ioprio)) {
1266                 new_ioc = get_task_io_context(tsk, GFP_KERNEL, NUMA_NO_NODE);
1267                 if (unlikely(!new_ioc))
1268                         return -ENOMEM;
1269 
1270                 new_ioc->ioprio = ioc->ioprio;
1271                 put_io_context(new_ioc);
1272         }
1273 #endif
1274         return 0;
1275 }
```

ここで注意が必要で、**ioprio_valid(ioc->ioprio)**が0なら新しいtskはio_contextが初期化されずNULLのままとなる。**ioprio_valid()**は[kernel/include/linux/ioprio.h](http://lxr.free-electrons.com/source/include/linux/ioprio.h?v=4.10#17)より、

```c:ioprio.h
17 #define ioprio_valid(mask)      (IOPRIO_PRIO_CLASS((mask)) != IOPRIO_CLASS_NONE)
```

となる。つまり、clone()時に、親がio_context==NULLのままもしくは**IOPRIO_CLASS_NONE**なら、子はio_context==NULLとなる。(**CLONE_IO**の時は親と子がio_contextを共用する...けど親がio_context==NULLのままだと共用せず、親も子も初回作ろうとしたときにそれぞれ作るな、これもバグか。)

# まとめ
 - 「**none: prio 4**」はio_contextが初期化されていない状態を表す。
 - 「**none: prio 0**」はio_contextが初期化されIOPRIO_CLASS_NONEに設定されている状態を表す。
 - 「**none: prio 0**」な人がfork()やpthread_create()すると子が「**none: prio 4**」になる
 - どちらもユーザランドから見た動きの差はない (io_contextがNULLなら初回request出す時にIOPRIO_CLASS_NONEで初期化されるため)

# あとがき
[先のI/OスケジューラのクラスとCFQの記事](http://qiita.com/rarul/items/3d80436a9fea90445cdb)に絡み、ioprio_get(2)しまくるテストをしているときに気づいた問題から出た疑問をまとめた。なにか問題が起こってioprio値が不正規にでもなってしまったかと思い真剣に調べてしまったが、ioprioの管理の中だけに閉じた特に実害のないしょうもないバグが原因だった。

ちなみに、sched(7)の各種設定値は[**/proc/[pid]/stat**](https://linuxjm.osdn.jp/html/LDP_man-pages/man5/proc.5.html)から取れるが、ioprioの値はここからは取れず、現時点ではioprio_get(2)のsyscallをしないといけない。/proc/[0-9]+を見ながらioprio_get(2)しまくるツールという形で久々にプログラムを書いた気がする。

kernelへのパッチの出し方よく知らないので、代わりに誰か、IOPRIO_NORMを0に変更する修正を出しませんか？