Erlang Thursday - ETS介绍第四篇:ETS的访问保护

今天的Erlang Thursday继续介绍ETS并研究ETS支持的不同访问级别。

ETS支持的不同访问级别:public,protected和private。

在创建一个新ETS表的时候可以传入不同访问类型的任意一个,不过我们先看看当我们不指定访问级别的时候ETS表的访问级别是哪一个?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Table = ets:new(some_name, []).
% 20501
ets:info(Table).
% [{read_concurrency,false},
% {write_concurrency,false},
% {compressed,false},
% {memory,305},
% {owner,<0.81.0>},
% {heir,none},
% {name,some_name},
% {size,0},
% {node,nonode@nohost},
% {named_table,false},
% {type,set},
% {keypos,1},
% {protection,protected}]

从上面的输出可以看到不指定访问级别的话默认就是protected。

那么一个ETS表是被保护的意味着什么呢?官方文档说明被保护的表只可以被所有者进程写入,但是其他进程可以读取。

现在让我们一起看看它是如何工作的。

首先我们创建一个进程以便我们可以把ETS表转移给它。

1
2
3
4
Fun = fun() -> receive after infinity -> ok end end.
% #Fun<erl_eval.20.54118792>
SomeProcess = spawn(Fun).
% <0.58.0>

我们创建一个新的ETS表并指定它是被保护的,同时也指定它是有名字的以方便后续操作。

1
2
ProtectedNamedETS = ets:new(protected_named_ets, [protected, named_table]).
% protected_named_ets

函数的输出是 protected_name_ets 而不是像前面调用 ets:new/2 那样输出的是数字,这样一来我们能够用表的名字替代表标识码访问这个表。

我们将插入一条数据到这个ETS表里,并且我们将用这个ETS表的名字作为它的引用因为我们创建表的时候指定了 named_table 选项。

1
2
ets:insert(protected_named_ets, {foobar, baz}).
% true

ets:insert/2 返回true,那么现在我们应该有一些数据在表里。让我们用ets:match/2把数据取出来,而且通过用一个$1的模式匹配出所有数据。

1
2
ets:match(protected_named_ets, '$1').
% [[{foobar,baz}]]

那么作为这个ETS表的所有者进程,因为这个表是由这个进程创建的,所有我们能读写这个表。

现在我们把这个表转移给另外的进程。

1
2
ets:give_away(protected_named_ets, SomeProcess, []).
% true

既然官方文档说表是可读的,我们在刚刚转移所有权后做同样的match操作。

1
2
ets:match(protected_named_ets, '$1').
% [[{foobar,baz}]]

我们读取到我们的数据。
那么尝试写会发生什么?因为官方文档说只有所有者进程才能有写的权限,并且在调用 ets:insert/2 的时候总是返回true。

1
2
3
4
ets:insert(protected_named_ets, {barbaz, foo}).
% ** exception error: bad argument
% in function ets:insert/2
% called as ets:insert(protected_named_ets,{barbaz,foo})

上面的例子返回了一个异常,异常的类型是 bag argument,也就是说它不允许非所有者进程写数据入表,但是这个异常没有确切地说明到底发生了什么。

如果我们尝试调用 ets:insert/2 往不存在的表插入数据将会发生什么?

1
2
3
4
ets:insert(no_such_table, {foo, bar}).
% ** exception error: bad argument
% in function ets:insert/2
% called as ets:insert(no_such_table,{foo,bar})

一样的异常和一样的错误提示格式,仅仅是表名和元组不同。

仔细想想这些现象,这两种不同的情况有一样的错误是有意义的。当一个进程尝试去做一个插入而如果没有表存在或者如果表被设置为 protected,则就是要让正在调用插入动作的这个进程知道这样的表不存在。总之,就是调用者将一个错的ETS表的引用传给ets:insert/2 。

所以我们现在已经知道 protected 的行为,它是默认的访问级别,那么下面让我们看看 public 。

1
2
PublicNamedETS = ets:new(public_named_ets, [public, named_table]).
% public_named_ets

我们将从我们当前的进程,也就是表的所有者插入一条数据并且获取所有数据。

1
2
3
4
ets:insert(public_named_ets, {foo, bar}).
% true
ets:match(public_named_ets, '$1').
% [[{foo,bar}]]

一切正常。

官方文档说public的表允许任何进程读和写,所以让我们把这个public表转给进程 SomeProcess 并且尝试去读和写。

1
2
ets:give_away(public_named_ets, SomeProcess, []).
% true

现在我们已经将表转移出去,是时候来尝试添加一条新的数据到表里,同时来看看我们能否将写入的内容读取出来。

1
2
3
4
ets:insert(public_named_ets, {bar, baz}).
% true
ets:match(public_named_ets, '$1').
% [[{foo,bar}],[{bar,baz}]]

完全没问题。我们已经插入新数据到那个表里,并且当我们调用 ets:match/2 获取所有数据的时候,我们看到新数据在结果集里。

现在让我们创建一个 private 表。官方文档说对于 private 的ETS表,只有表的所有者才被允许读写这个ETS表。

1
2
PrivateNamedETS = ets:new(private_named_ets, [private, named_table]).
private_named_ets

当进程还拥有这个表的时候,我们添加一条数据并读取出来。

1
2
3
4
ets:insert(private_named_ets, {fizz, buzz}).
% true
ets:match(private_named_ets, '$1').
% [[{fizz,buzz}]]

然后我们又把表转移给进程 SomeProcess。

1
2
ets:give_away(private_named_ets, SomeProcess, []).
% true

现在这个ETS表属于另一个进程了,我们再来尝试读取它的数据。

1
2
3
4
ets:match(private_named_ets, '$1').
% ** exception error: bad argument
% in function ets:match/2
% called as ets:match(private_named_ets,'$1')

又是 bad argument 异常,就像前面例子我们尝试在一个 protected ETS表上用 ets:insert/2 函数,而当时那个表属于另外一个进程。
再来看看写的情况。

1
2
3
4
ets:insert(private_named_ets, {buzz, fizz}).
% ** exception error: bad argument
% in function ets:insert/2
% called as ets:insert(private_named_ets,{buzz,fizz})

也是 bad argument 异常,现在这样的情况不会让人奇怪了,因为 protected 表的写以及这个 private 表的读都造成一样的异常。

总之,在所有到目前为止的ETS的介绍文章里,我们已经见过了ETS表的类型、访问级别、表命名、继承人和所有者这些属性的设置以及它们之间的关系。

下星期,我们将通过介绍ETS表的键位置的设置和其他一些设置来结束ETS介绍系列文章。

原文链接: https://www.proctor-it.com/erlang-thursday-ets-introduction-part-4-ets-access-protections/