この記事について
Ruby3.x にて使用可能な FiberScheduler#address_resolve
を自前実装している。
このメソッドが返す値がRuby言語の中でどのように使用されるのかを確認したい。
FiberScheduler#address_resolve
について
以下の通り。
調査内容
自分が実装するスケジューラにおいて、FiberScheduler#address_resolve
が、["1.1.1.1"]
を返すように実装してみた。
class AsyncScheduler
def address_resolve
["1.1.1.1"]
end
end
すると、このスケジューラを利用したSocket.getaddrinfo("www.google.com", 443)
は、
Fiber.set_scheduler AsyncScheduler.new
Fiber.schedule do
Socket.getaddrinfo("www.google.com", 443) ## この箇所
end
以下の値を返すようになった。
[
["AF_INET", 443, "1.1.1.1", "1.1.1.1", 2, 1, 6],
["AF_INET", 443, "1.1.1.1", "1.1.1.1", 2, 2, 17],
["AF_INET", 443, "1.1.1.1", "1.1.1.1", 2, 3, 0]
]
以下2点を知りたい。
-
2, 1, 6
,2, 2, 17
,2, 3, 0
は何を意味するのか? - Ruby内部のどういった処理によりこれらの3つの値が返っているのか?
調査(1) 2, 1, 6
, 2, 2, 17
, 2, 3, 0
は何を意味するのか?
singleton method Socket.getaddrinfo
アドレス情報とは7つの要素からなる次の形の配列です。第0要素 - アドレスファミリー (String)
第1要素 - ポート番号 (Integer)
第2要素 - ホスト名 (String)
第3要素 - アドレス (String)
第4要素 - アドレスファミリーに対応する Integer
第5要素 - ソケットタイプに対応する Integer
第6要素 - プロトコルに対応する Integer
https://docs.ruby-lang.org/ja/3.1/method/Socket/s/getaddrinfo.html
つまり、
address family | socket type | protocol | |
---|---|---|---|
2, 1, 6 |
AF_INET (IPv4) |
SOCK_STREAM |
IPPROTO_TCP |
2, 2, 17 |
AF_INET (IPv4) |
SOCK_DGRAM |
IPPROTO_UDP |
2, 3, 0 |
AF_INET (IPv4) |
SOCK_RAW |
IPPROTO_IP |
Socket::Constants
https://docs.ruby-lang.org/ja/3.1/class/Socket.html
-
Socket::Constants::AF_INET = 2
-
Socket::Constants::SOCK_STREAM = 1
-
Socket::Constants::SOCK_DGRAM = 2
-
Socket::Constants::SOCK_RAW = 3
-
Socket::Constants::IPPROTO_IP = 0
-
Socket::IPPROTO_TCP = 6
-
Socket::Constants::IPPROTO_UDP = 17
なお、SOCK_RAW
の説明は以下である。
SOCK_RAW
内部ネットワーク・プロトコルおよびインターフェースへのアクセスを提供します。 このタイプのソケットは、root ユーザー権限を持つユーザー、または CAP_NUMA_ATTACH 機能を持つ非 root ユーザーのみが使用できます。 (非 root ロー・ソケット・アクセスの場合、 chuser コマンドは CAP_PROPAGATEとともに CAP_NUMA_ATTACH 機能を割り当てます。 詳しくは、 chuser コマンドを参照してください。)ロー・ソケットを使用すると、アプリケーションは下位レベルの通信プロトコルに直接アクセスできます。 ロー・ソケットは、通常のインターフェースを介して直接アクセスできないプロトコル機能を利用したい場合や、既存の低レベル・プロトコルの上に新しいプロトコルを構築したい場合に、上級者を対象としています。
ロー・ソケットは通常、データグラム指向ですが、正確な特性は、プロトコルによって提供されるインターフェースによって異なります。
https://www.ibm.com/docs/ja/aix/7.3?topic=protocols-socket-types
調査(2) Ruby内部のどういった処理によりこれらの3つの値が返っているのか?
https://github.com/ruby/ruby を読む。
折角なので、手元にRubyのソースコードを落としてきて、lldbでブレイクポイントを仕掛けてみる。
に従う。
require 'socket'
require '/Users/daiki-kudo/repos/nonblocking-resolv/lib/nonblocking/resolv'
require '/Users/daiki-kudo/repos/async_scheduler/lib/async_scheduler'
scheduler = AsyncScheduler::Scheduler.new
Fiber.set_scheduler scheduler
x = nil
Fiber.schedule do
x = Socket.getaddrinfo("www.google.com", 443) ## この箇所
end
scheduler.close
print x
$make lldb-ruby
で実行する。
($make lldb
では、実行不可だった。外部gemが使用できないとどこかで読んだ記憶がある。)
$ make runruby
RUBY_ON_BUG='gdb -x ../.gdbinit -p' ./miniruby -I../lib -I. -I.ext/common ../tool/runruby.rb --extout=.ext -- --disable-gems ../test.rb
[["AF_INET", 443, "1.1.1.1", "1.1.1.1", 2, 1, 6],
["AF_INET", 443, "1.1.1.1", "1.1.1.1", 2, 2, 17],
["AF_INET", 443, "1.1.1.1", "1.1.1.1", 2, 3, 0]]%
さて、とりあえず、FiberScheduler#address_resolve
を呼び出しているCレイヤーの scheduler.c#rb_fiber_scheduler_address_resolveにブレイクポイントを設置する。ここで、コールスタックを見て、どのような順番でメソッド呼び出しがされているかを調べる。
$ make lldb-ruby
(lldb) br set --name rb_fiber_scheduler_address_resolve
Breakpoint 1: where = ruby`rb_fiber_scheduler_address_resolve + 22 at scheduler.c:673:25, address = 0x00000001001c2746
(lldb) r
Process 32213 launched: '/Users/daiki-kudo/repos/ruby/build/ruby' (x86_64)
Process 32213 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00000001001c2746 ruby`rb_fiber_scheduler_address_resolve(scheduler=4326338320, hostname=4376285200) at scheduler.c:673:25
670 VALUE
671 rb_fiber_scheduler_address_resolve(VALUE scheduler, VALUE hostname)
672 {
-> 673 VALUE arguments[] = {
674 hostname
675 };
676
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001001c2746 ruby`rb_fiber_scheduler_address_resolve(scheduler=4326338320, hostname=4376285200) at scheduler.c:673:25
frame #1: 0x0000000104c8df66 socket.bundle`rsock_getaddrinfo [inlined] rb_scheduler_getaddrinfo(scheduler=<unavailable>, host=4376285200, service=<unavailable>, hints=0x0000000104c1fa70, res=<unavailable>) at raddrinfo.c:478:26
frame #2: 0x0000000104c8df51 socket.bundle`rsock_getaddrinfo(host=4376285200, port=887, hints=0x0000000104c1fa70, socktype_hack=0) at raddrinfo.c:545:21
frame #3: 0x0000000104c85c94 socket.bundle`sock_s_getaddrinfo(argc=<unavailable>, argv=<unavailable>, _=<unavailable>) at socket.c:1186:11
frame #4: 0x0000000100255c64 ruby`vm_call_cfunc_with_frame_(ec=0x0000000104e05ed0, reg_cfp=0x0000000104c3ff90, calling=<unavailable>, argc=2, argv=0x0000000104c20038, stack_bottom=0x0000000104c20030) at vm_insnhelper.c:3468:11
frame #5: 0x000000010023a431 ruby`vm_exec_core [inlined] vm_sendish(ec=0x0000000104e05ed0, reg_cfp=<unavailable>, cd=<unavailable>, block_handler=0, method_explorer=mexp_search_method) at vm_insnhelper.c:5546:15
frame #6: 0x000000010023a374 ruby`vm_exec_core(ec=0x0000000104e05ed0) at insns.def:835:11
frame #7: 0x0000000100236b92 ruby`rb_vm_exec(ec=0x0000000104e05ed0) at vm.c:2406:22
frame #8: 0x000000010024ab7f ruby`vm_invoke_proc [inlined] invoke_block(ec=0x0000000104e05ed0, iseq=0x0000000101e04578, self=4301835640, captured=<unavailable>, cref=0x0000000000000000, type=<unavailable>, opt_pc=<unavailable>) at vm.c:1441:12
frame #9: 0x000000010024aa7e ruby`vm_invoke_proc [inlined] invoke_iseq_block_from_c(ec=0x0000000104e05ed0, captured=<unavailable>, self=4301835640, argc=<unavailable>, argv=<unavailable>, kw_splat=<unavailable>, passed_block_handler=<unavailable>, cref=0x0000000000000000, is_lambda=<unavailable>, me=0x0000000000000000) at vm.c:1511:16
frame #10: 0x000000010024a91f ruby`vm_invoke_proc [inlined] invoke_block_from_c_proc(ec=<unavailable>, proc=<unavailable>, self=<unavailable>, argc=<unavailable>, argv=<unavailable>, kw_splat=<unavailable>, passed_block_handler=<unavailable>, is_lambda=<unavailable>, me=0x0000000000000000) at vm.c:1609:16
frame #11: 0x000000010024a7fd ruby`vm_invoke_proc(ec=0x0000000104e05ed0, proc=<unavailable>, self=4301835640, argc=<unavailable>, argv=<unavailable>, kw_splat=<unavailable>, passed_block_handler=0) at vm.c:1639:12
frame #12: 0x000000010024a79a ruby`rb_vm_invoke_proc(ec=<unavailable>, proc=<unavailable>, argc=<unavailable>, argv=<unavailable>, kw_splat=<unavailable>, passed_block_handler=<unavailable>) at vm.c:1660:16
frame #13: 0x0000000100078d20 ruby`rb_fiber_start(fiber=0x0000000104e05e80) at cont.c:2525:23
frame #14: 0x000000010007b8c1 ruby`fiber_entry(from=<unavailable>, to=<unavailable>) at cont.c:843:5
つまり、コールスタックは上から順に
- scheduler.c:673 #rb_fiber_scheduler_address_resolve
- raddrinfo.c:478 #rb_scheduler_getaddrinfo
- raddrinfo.c:545 #rsock_getaddrinfo
- socket.c:1186 #sock_s_getaddrinfo
ここで、raddrinfo.c#rb_scheduler_getaddrinfo を抜粋すると、以下。
FiberScheduler#address_resolve
から得られたIPアドレスの配列(今回は ["1.1.1.1"]
)に対してループを周している。
それぞれについて、#numeric_getaddrinfo
を呼び出して得られた addrinfoのLinkedListを、同じくLinkedListである(*res)->ai
の末尾にくっつけている。
static int
rb_scheduler_getaddrinfo(VALUE scheduler, VALUE host, const char *service,
const struct addrinfo *hints, struct rb_addrinfo **res)
{
long i, len;
struct addrinfo *ai, *ai_tail = NULL;
// FiberScheduler#address_resolve から得られたIPアドレスの配列
ip_addresses_array = rb_fiber_scheduler_address_resolve(scheduler, host);
len = RARRAY_LEN(ip_addresses_array);
for(i=0; i<len; i++) {
ip_address = rb_ary_entry(ip_addresses_array, i);
hostp = host_str(ip_address, _hbuf, sizeof(_hbuf), &_additional_flags);
// ai には、address_info構造体のLinkedListの先頭が代入される。
// 後述のように、このLinkedListは、SOCK_STREAM, SOCK_DGRAM, SOCK_RAWに対応する3つの要素を持つ。
error = numeric_getaddrinfo(hostp, service, hints, &ai);
if (!res_allocated) {
res_allocated = 1;
*res = (struct rb_addrinfo *)xmalloc(sizeof(struct rb_addrinfo));
(*res)->allocated_by_malloc = 1;
(*res)->ai = ai; // res に address_info構造体のLinkedListの先頭 を代入
ai_tail = ai;
} else {
while (ai_tail->ai_next) {
ai_tail = ai_tail->ai_next;
}
ai_tail->ai_next = ai; // LinkedListの末尾に要素を追加
ai_tail = ai;
}
}
addrinfo構造体の型
struct addrinfo {
int ai_flags; /* AI_PASSIVE, AI_CANONNAME */
int ai_family; /* PF_xxx */
int ai_socktype; /* SOCK_xxx */
int ai_protocol; /* 0 or IPPROTO_xxx for IPv4 and IPv6 */
size_t ai_addrlen; /* length of ai_addr */
char *ai_canonname; /* canonical name for hostname */
struct sockaddr *ai_addr; /* binary address */
struct addrinfo *ai_next; /* next structure in linked list */
};
raddrinfo.c#numeric_getaddrinfoを抜粋すると以下。
確かに、SOCK_STREAM
, SOCK_DGRAM
, SOCK_RAW
の3種類がlistとして定義されており、ループを回して、これらに対応するaddress_info構造体からなるLinkedListがai
に代入されている。
static int
numeric_getaddrinfo(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **res)
{
static const struct {
int socktype;
int protocol;
} list[] = {
{ SOCK_STREAM, IPPROTO_TCP }, // ここ!!!
{ SOCK_DGRAM, IPPROTO_UDP },
{ SOCK_RAW, 0 }
};
for (i = numberof(list)-1; 0 <= i; i--) {
if ((hint_socktype == 0 || hint_socktype == list[i].socktype) &&
(hint_protocol == 0 || list[i].protocol == 0 || hint_protocol == list[i].protocol)) {
// ai は、LinkedListの先頭のNode
// ai0 は、今から新しく先頭に挿入するNode
struct addrinfo *ai0 = xcalloc(1, sizeof(struct addrinfo)); struct sockaddr_in *sa = xmalloc(sizeof(struct sockaddr_in));
INIT_SOCKADDR_IN(sa, sizeof(struct sockaddr_in));
memcpy(&sa->sin_addr, ipv4addr, sizeof(ipv4addr));
sa->sin_port = htons(port);
ai0->ai_family = PF_INET;
ai0->ai_socktype = list[i].socktype;
ai0->ai_protocol = hint_protocol ? hint_protocol : list[i].protocol;
ai0->ai_addrlen = sizeof(struct sockaddr_in);
ai0->ai_addr = (struct sockaddr *)sa;
ai0->ai_canonname = NULL;
// ai0を先頭に挿入
ai0->ai_next = ai;
ai = ai0;
}
}
if (ai) {
*res = ai;
return 0;
}
}