[Erlang 0041] 详解io:format-阿里云开发者社区

开发者社区> 开发与运维> 正文

[Erlang 0041] 详解io:format

简介:
 最近遇到几个问题,都是和Erlang Shell输出有关,问题解决了但是追问还要继续下去,后面几篇文章都将围绕这一话题展开;那我们就从io:format("hello world!")开始说起吧.
复制代码
%%代码路径:\erl5.9\lib\stdlib-1.18\src\io.erl
format(Format) ->
format(Format, []).

format(Format, Args) ->
format(default_output(), Format, Args).

format(Io, Format, Args) ->
o_request(Io, {format,Format,Args}, format).
复制代码

 打开\erl5.9\lib\stdlib-1.18\src\io.erl,显然io:format("hello world!")的调用会走到format(default_output(), Format, Args).我们的第一个问题就是这里的default_output,它的实现很简单:
default_output() ->
group_leader().
 这里的group_leader/0,实际上是erlang:group_leader().我们看官方文档对它的解释:

group_leader() -> GroupLeader

Types:
GroupLeader = pid()
     Returns the pid of the group leader for the process which evaluates the function.
   Every process is a member of some process group and all groups have a group leader. All IO from the group is channeled to the group leader. When a new process is spawned, it gets the same group leader as the spawning process. Initially, at system start-up, init is both its own group leader and the group leader of all processes.

 Erlang进程不是孤立的,进程都属于进程组,进程组都有group leader.所有的进程组的IO都会重定向到group leader.当一个进程被创建的时候,它就会继承父进程的group leader.系统初始化的时候,init是它自己和其它所有进程的group leader.我们下面在Erlang Shell中做一下检验:
1.在shell中创建一个进程,它的group_leader是什么?
2.当前shell的group leader是什么?
3.init进程的group leader
4.搞崩这个shell,让shell重启,我们继续观察
复制代码
Eshell V5.9  (abort with ^G)
1> self().
<0.30.0>
2> P=spawn(fun()-> receive after infinity -> hello end end ).
<0.33.0>
3> erlang:process_info(P).
[{current_function,{erl_eval,receive_clauses,8}},
{initial_call,{erlang,apply,2}},
{status,waiting},
{message_queue_len,0},
{messages,[]},
{links,[]},
{dictionary,[]},
{trap_exit,false},
{error_handler,error_handler},
{priority,normal},
{group_leader,<0.23.0>}, %%注意这里就是进程P的group leader
{total_heap_size,233},
{heap_size,233},
{stack_size,10},
{reductions,18},
{garbage_collection,[{min_bin_vheap_size,46368},
{min_heap_size,233},
{fullsweep_after,65535},
{minor_gcs,0}]},
{suspending,[]}]
4> erlang:process_info(pid(0,23,0)). %%继续跟进看看P进程的group leader是什么样的进程?
[{registered_name,user}, %%注意P进程的group leader的registered_name是user!!!
{current_function,{user,server_loop,2}},
{initial_call,{erlang,apply,2}},
{status,waiting},
{message_queue_len,0},
{messages,[]},
{links,[<0.21.0>,<0.24.0>,#Port<0.319>,<0.5.0>]},
{dictionary,[{unicode,false},
{read_mode,list},
{shell,<0.24.0>}]},
{trap_exit,true},
{error_handler,error_handler},
{priority,normal},
{group_leader,<0.23.0>}, %%user进程的group leader就是它自己
{total_heap_size,3194},
{heap_size,2584},
{stack_size,9},
{reductions,1310},
{garbage_collection,[{min_bin_vheap_size,46368},
{min_heap_size,233},
{fullsweep_after,65535},
{minor_gcs,2}]},
{suspending,[]}]
5> whereis(init). %%观察下init进程的元数据
<0.0.0>
6> erlang:process_info(pid(0,0,0)).
[{registered_name,init},
{current_function,{init,loop,1}},
{initial_call,{otp_ring0,start,2}},
{status,waiting},
{message_queue_len,0},
{messages,[]},
{links,[<0.5.0>,<0.6.0>,<0.3.0>]},
{dictionary,[]},
{trap_exit,true},
{error_handler,error_handler},
{priority,normal},
{group_leader,<0.0.0>},%init的group leader 就是它自己
{total_heap_size,1974},
{heap_size,1597},
{stack_size,2},
{reductions,2357},
{garbage_collection,[{min_bin_vheap_size,46368},
{min_heap_size,233},
{fullsweep_after,65535},
{minor_gcs,4}]},
{suspending,[]}]
7> erlang:process_info(pid(0,30,0)). %调转回头我们看看当前这个shell的group leader
[{current_function,{erl_eval,do_apply,6}},
{initial_call,{erlang,apply,2}},
{status,running},
{message_queue_len,0},
{messages,[]},
{links,[<0.24.0>]},
{dictionary,[]},
{trap_exit,false},
{error_handler,error_handler},
{priority,normal},
{group_leader,<0.23.0>}, %还记得这是什么进程的进程ID? 对,是user
{total_heap_size,3571},
{heap_size,2584},
{stack_size,24},
{reductions,18941},
{garbage_collection,[{min_bin_vheap_size,46368},
{min_heap_size,233},
{fullsweep_after,65535},
{minor_gcs,6}]},
{suspending,[]}]
8> self(). %%下面我们要把当前shell搞崩
<0.30.0>
9> 1/0.
** exception error: bad argument in an arithmetic expression
in operator '/'/2
called as 1 / 0
10> self(). %%再次查看 Shell的pid已经变了
<0.41.0>
11> erlang:process_info(pid(0,41,0)).
[{current_function,{erl_eval,do_apply,6}},
{initial_call,{erlang,apply,2}},
{status,running},
{message_queue_len,0},
{messages,[]},
{links,[<0.24.0>]},
{dictionary,[]},
{trap_exit,false},
{error_handler,error_handler},
{priority,normal},
{group_leader,<0.23.0>}, %%注意这里的group leader 还是shell
{total_heap_size,3571},
{heap_size,2584},
{stack_size,24},
{reductions,3281},
{garbage_collection,[{min_bin_vheap_size,46368},
{min_heap_size,233},
{fullsweep_after,65535},
{minor_gcs,8}]},
{suspending,[]}]
12>
复制代码
   系统启动的时候,init进程首先被创建(Pid <0.0.0>),是自己的group leader,前面说过它还是所有进程的group leader,逻辑上是这样的,因为它首先被创建.在shell中我们创建的进程以及shell进程,group leader都是user进程!
  user进程是做什么用的呢?我们看下官方文档中的描述:

user 
Standard I/O Server
DESCRIPTION
user is a server which responds to all the messages defined in the I/O interface. The code in user.erl can be used as a model for building alternative I/O servers.

 原来user是标准的I/O 的server,看一下user进程的创建过程片段:
复制代码
%%代码路径\erl5.9\lib\kernel-2.15\src\user.erl
run(P) ->
put(read_mode,list),
put(unicode,false),
case init:get_argument(noshell) of
%% non-empty list -> noshell
{ok, [_|_]} ->
put(shell, noshell),
server_loop(P, queue:new());
_ ->
group_leader(self(), self()),
catch_loop(P, start_init_shell())
end.
复制代码
可以看到这里调用了 group_leader(self(), self())方法,看下这个方法的作用:
group_leader(GroupLeader, Pid) -> true
 Types:
GroupLeader = Pid = pid()

Sets the group leader of Pid to GroupLeader. Typically, this is used when a processes started from a certain shell should have another group leader than init.
See also group_leader/0.

这个方法的作用是:把某进程(Pid)的group leader 设置为GroupLeader,上面user执行的 group_leader(self(), self())就是把group leader设置为自己.
 
  到目前为止,我们还没有把io:format的整个过程走完,转回头继续看io:fromat的实现,现在进行到 o_request(Io, {format,Format,Args}, format).继续跟进:
复制代码
%%代码路径:\erl5.9\lib\stdlib-1.18\src\io.erl
o_request(Io, Request, Func) ->
case request(Io, Request) of %这里的Io参数的值就是group_leader哦
{error, Reason} ->
[_Name | Args] = tuple_to_list(to_tuple(Request)),
{'EXIT',{get_stacktrace,[_Current|Mfas]}} = (catch erlang:error(get_stacktrace)),
erlang:raise(error, conv_reason(Func, Reason), [{io, Func, [Io | Args]}|Mfas]);
Other ->
Other
end.

request(Request) ->
request(default_output(), Request).

request(standard_io, Request) ->
request(group_leader(), Request);
request(Pid, Request) when is_pid(Pid) -> %%看这里 我们走进的是这个分支
execute_request(Pid, io_request(Pid, Request)); %%io_request/2方法是一个消息格式转换的方法 它的实现摘录在后面
request(Name, Request) when is_atom(Name) ->
case whereis(Name) of
undefined ->
{error, arguments};
Pid ->
request(Pid, Request)
end.

execute_request(Pid, {Convert,Converted}) -> %%然后是到了这里
Mref = erlang:monitor(process, Pid),
Pid ! {io_request,self(),Pid,Converted}, %%这里向group_leader 发送一个消息,我们看看user进程接收到这个消息之后做了什么
if
Convert ->
convert_binaries(wait_io_mon_reply(Pid, Mref));
true ->
wait_io_mon_reply(Pid, Mref)
end.
复制代码
上面的代码跟踪过程,最后看到了向group_leader发送已经格式化的消息,下面继续跟踪到user.erl,看这个消息的接收与处理
复制代码
%%代码路径\erl5.9\lib\kernel-2.15\src\user.erl
server_loop(Port, Q) ->
receive
{io_request,From,ReplyAs,Request} when is_pid(From) ->
server_loop(Port, do_io_request(Request, From, ReplyAs, Port, Q));
{Port,{data,Bytes}} ->
case get(shell) of
noshell ->
server_loop(Port, queue:snoc(Q, Bytes));
_ ->
case contains_ctrl_g_or_ctrl_c(Bytes) of
false ->
server_loop(Port, queue:snoc(Q, Bytes));
_ ->
throw(new_shell)
end
end;
{Port, eof} ->
put(eof, true),
server_loop(Port, Q);

%% Ignore messages from port here.
{'EXIT',Port,badsig} -> % Ignore badsig errors
server_loop(Port, Q);
{'EXIT',Port,What} -> % Port has exited
exit(What);

%% Check if shell has exited
{'EXIT',SomePid,What} ->
case get(shell) of
noshell ->
server_loop(Port, Q); % Ignore
_ ->
throw({unknown_exit,{SomePid,What},Q})
end;

_Other -> % Ignore other messages
server_loop(Port, Q)
end.
复制代码
 代码里面出现了shell的身影,user进程的进程指点中会保留当前shell的Pid,这样我们的io_server user就知道最终输出在什么终端上了;进行一个实验,我们查看一下user的元数据然后搞崩shell,看看这时候user的进程字典是什么情况:
复制代码
Eshell V5.9  (abort with ^G)
1> whereis(user).
<0.23.0>
2> erlang:process_info(whereis(user)).
[{registered_name,user},
{current_function,{user,server_loop,2}},
{initial_call,{erlang,apply,2}},
{status,waiting},
{message_queue_len,0},
{messages,[]},
{links,[<0.21.0>,<0.24.0>,#Port<0.319>,<0.5.0>]},
{dictionary,[{unicode,false},
{read_mode,list},
{shell,<0.24.0>}]},
{trap_exit,true},
{error_handler,error_handler},
{priority,normal},
{group_leader,<0.23.0>},
{total_heap_size,987},
{heap_size,610},
{stack_size,9},
{reductions,666},
{garbage_collection,[{min_bin_vheap_size,46368},
{min_heap_size,233},
{fullsweep_after,65535},
{minor_gcs,5}]},
{suspending,[]}]
3> exit(pid(0,24,0),kill).
*** ERROR: Shell process terminated! ***
Eshell V5.9 (abort with ^G)
1> whereis(user).
<0.23.0>
2> erlang:process_info(whereis(user)).
[{registered_name,user},
{current_function,{user,server_loop,2}},
{initial_call,{erlang,apply,2}},
{status,waiting},
{message_queue_len,0},
{messages,[]},
{links,[<0.5.0>,<0.21.0>,<0.34.0>,#Port<0.319>]},
{dictionary,[{unicode,false},
{read_mode,list},
{shell,<0.34.0>}]},
{trap_exit,true},
{error_handler,error_handler},
{priority,normal},
{group_leader,<0.23.0>},
{total_heap_size,1364},
{heap_size,987},
{stack_size,9},
{reductions,1659},
{garbage_collection,[{min_bin_vheap_size,46368},
{min_heap_size,233},
{fullsweep_after,65535},
{minor_gcs,8}]},
{suspending,[]}]
3>
复制代码

  到这里io:format执行的整个流程比较清晰了:向group_leader 按照指定格式发送io请求,group leader负责io请求的处理,通常情况下的group leader是user,user进程中维护了输出终端shell的进程pid,Shell重建之后user会更新进程字典.现在我们已经可以做一些有趣的事情了,比如不使用io:format直接向user进程发送请求,就像下面这样:
复制代码
Eshell V5.9  (abort with ^G)
1> U =whereis(user).
<0.23.0>
2> U!{io_request,self(),self(), {put_chars,unicode,io_lib,format, ["hello world
:~p~n
",[zen]]}}.
hello world :zen
{io_request,<0.30.0>,<0.30.0>,
{put_chars,unicode,io_lib,format,
["hello world :~p~n",[zen]]}}
3>
复制代码
 当然也可以解决一些实际的问题了,比如erlangqa上的这个问题:


litaocheng已经给出了解决方案,现在我们看这个解决方案就不再陌生了吧:
通过修改group_leader,达到io重定向的目的.
比如代码:
cat test.erl 
复制代码
-module(test).
-compile([export_all]).

r() ->
io:format("group leader:~p~n", [erlang:group_leader()]),
io:format("node:~p~n", [node()]),
erlang:group_leader(whereis(user), self()),
io:format("hello world~n").
复制代码
随后:
erl -sname t1
erl -sname t2
在t1中执行:
net_kernel:connect_node('t2@litao').
rpc:call('t2@litao', test, r, []).
会看到t2中输出hello world

 可能注意到我们的代码止于user进程接收到消息并没有继续下去,这是因为后面的代码跟踪会完全陷入io_protocol的细节里面去.我们简单看下:
The Erlang I/O-protocol
完整协议请点击:http://erlang.org/doc/apps/stdlib/io_protocol.html

关于io输出的话题,远远没有结束,还有很多问题值得思考,比如rpc:all的时候io输出是怎么控制的?OTP application启动之后的group leader是怎样的?JCL方式接入一个节点,输出又是怎样一个流程?所以,今天先到这里,未完待续

io 模块 online documentation 

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章