Skip to content

Commit

Permalink
Merge pull request #333 from alexandergall/learning-bridge
Browse files Browse the repository at this point in the history
Learning bridge based on Bloom filter
  • Loading branch information
lukego committed Dec 5, 2014
2 parents 72914ef + 8a33db3 commit 7e311e4
Show file tree
Hide file tree
Showing 10 changed files with 1,032 additions and 4 deletions.
88 changes: 88 additions & 0 deletions src/apps/bridge/base.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
-- Base class for an Ethernet bridge with split-horizon semantics.
--
-- A bridge conists of any number of ports, each of which is a member
-- of at most one split-horizon group. If it is not a member of a
-- split-horizon group, the port is also called a "free" port.
-- Packets arriving on a free port may be forwarded to all other
-- ports. Packets arriving on a port that belongs to a split-horizon
-- group are never forwarded to any port belonging to the same
-- split-horizon group.
--
-- The configuration is passed as a table of the following form
--
-- config = { ports = { <free-port1>, <free-port2>, ... },
-- split_horizon_groups = {
-- <sh_group1> = { <shg1-port1>, <shg1-port2>, ...},
-- ...},
-- config = { <bridge-specific-config> } }
--
-- The "config" table contains configuration options specific to a
-- derived class. It is ignored by the base class.
--
-- The base constructor checks the configuration and creates the
-- following arrays as private instance variables for efficient access
-- in the push() method (which must be provided by any derived class).
--
-- self._src_ports
--
-- This array contains the names of all ports connected to the
-- bridge.
--
-- self._dst_ports
--
-- This table is keyed by the name of an input port and associates
-- it with an array of output ports according to the split-horizon
-- topology.
--
-- The push() method of a derived class should iterate over all source
-- ports and forward the incoming packets to the associated output
-- ports, replicating the packets as necessary. In the simplest case,
-- the packets must be replicated to all destination ports (flooded)
-- to make sure they reach any potential recipient. A more
-- sophisticated bridge can store the MAC source addresses on incoming
-- ports to limit the scope of flooding.

module(..., package.seeall)

local bridge = subClass(nil)
bridge._name = "base bridge"

function bridge:new (config)
assert(self ~= bridge, "Can't instantiate abstract class "..self:name())
local o = bridge:superClass().new(self)
assert(config and config.ports, self:name()..": invalid configuration")

-- Create a list of forwarding ports for all ports connected to the
-- bridge, taking split horizon groups into account
local ports = {}
local function add_port(port, group)
assert(not ports[port],
self:name()..": duplicate definition of port "..port)
ports[port] = group
end
for _, port in ipairs(config.ports) do
add_port(port, '')
end
if config.split_horizon_groups then
for group, ports in pairs(config.split_horizon_groups) do
for _, port in ipairs(ports) do
add_port(port, group)
end
end
end
local src_ports, dst_ports = {}, {}
for sport, sgroup in pairs(ports) do
table.insert(src_ports, sport)
dst_ports[sport] = {}
for dport, dgroup in pairs(ports) do
if not (sport == dport or (sgroup ~= '' and sgroup == dgroup)) then
table.insert(dst_ports[sport], dport)
end
end
end
o._src_ports = src_ports
o._dst_ports = dst_ports
return o
end

return bridge
43 changes: 43 additions & 0 deletions src/apps/bridge/flooding.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
-- This class derives from lib.bridge.base and implements the simplest
-- possible bridge, which floods a packet arriving on a port to all
-- destination ports within its scope according to the split-horizon
-- topology.

module(..., package.seeall)

local bridge_base = require("apps.bridge.base")
local packet = require("core.packet")
local link = require("core.link")
local empty, receive, transmit = link.empty, link.receive, link.transmit
local cow_clone = packet.cow_clone

local bridge = subClass(bridge_base)
bridge._name = "flooding bridge"

function bridge:new (config)
return bridge:superClass().new(self, config)
end

function bridge:push()
local src_ports = self._src_ports
local dst_ports = self._dst_ports
local output = self.output
local i = 1
while src_ports[i] do
local src_port = src_ports[i]
local l_in = self.input[src_port]
while not empty(l_in) do
local ports = dst_ports[src_port]
local p = receive(l_in)
transmit(output[ports[1]], p)
local j = 2
while ports[j] do
transmit(output[ports[j]], cow_clone(p))
j = j + 1
end
end
i = i + 1
end
end

return bridge
192 changes: 192 additions & 0 deletions src/apps/bridge/learning.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
-- This class derives from lib.bridge.base and implements a "learning
-- bridge" using a Bloom filter (provided by lib.bloom_filter) to
-- store the set of MAC source addresses of packets arriving on each
-- port.
--
-- Two Bloom storage cells called mac_table and mac_shadow are
-- allocated for each port connected to the bridge. For each packet
-- arriving on a port, the MAC source address is stored in both cells.
-- The mac_table cell is used during packet forwarding while
-- mac_shadow is used to time out the learned addresses.
--
-- When a packet is received on a port, its MAC destination address is
-- looked up in the mac_table cells of all associated output
-- ports. The packet is sent on all ports for which the lookup results
-- in a match, replicating the packet if necessary. A destination can
-- be associated with multiple output ports, either because the
-- address has actually been learned on multiple ports or due to false
-- positives in the lookup operation, which are inevitable for Bloom
-- filters.
--
-- Multicast MAC addresses are always flooded to all output ports
-- associated with the input port.
--
-- The timing out of learned addresses is implemented by periodically
-- copying mac_shadow to mac_table and clearing mac_shadow for every
-- port. I.e., mac_table contains only the addresses learned during
-- the past timeout interval.
--
-- Configuration variables (via the "config" table in the generic
-- configuration of the base class)
--
-- mac_table_size (default 1000)
--
-- Expected maximum number of MAC addresses to store in each
-- per-port Bloom filter.
--
-- fp_rate (default 0.001)
--
-- Maximum rate of false-positives for lookups in the Bloom
-- filters, provided the number of distinct objects stored in the
-- filter does not exceed mac_table_size.
--
-- timeout (default 60 seconds)
--
-- Timeout for learned MAC addresses in seconds.
--
-- verbose (default false)
--
-- If true, a diagnostic message containing the storage cell usage
-- of each mac_table is printed to stdout
--

module(..., package.seeall)

local ffi = require("ffi")
local bridge_base = require("apps.bridge.base")
local packet = require("core.packet")
local link = require("core.link")
local bloom = require("lib.bloom_filter")
local ethernet = require("lib.protocol.ethernet")

local empty, receive, transmit = link.empty, link.receive, link.transmit
local cow_clone = packet.cow_clone

local bridge = subClass(bridge_base)
bridge._name = "learning bridge"

local default_config = { mac_table_size = 1000, fp_rate = 0.001,
timeout = 60, verbose = false }

function bridge:new (config)
local o = bridge:superClass().new(self, config)
local config = config.config or {}
for k, v in pairs(default_config) do
if not config[k] then
config[k] = v
end
end
local bf = bloom:new(config.mac_table_size, config.fp_rate)
o._bf = bf
o._nsrc_ports = #o._src_ports
o._port_index = 1
-- Per-port Bloom filters
o._filters = {}
for _, port in ipairs(o._src_ports) do
o._filters[port] = { mac_table = bf:cell_new(),
mac_shadow = bf:cell_new(),
mac_address = bf:item_new()
}

end
o._eth_dst = bf:item_new()

timer.activate(timer.new("mac_learn_timeout",
function (t)
if config.verbose then
print("MAC learning timeout")
print("Table usage per port:")
end
for port, filter in pairs(o._filters) do
bf:cell_copy(filter.mac_shadow, filter.mac_table)
bf:cell_clear(filter.mac_shadow)
if config.verbose then
print(string.format("\t%s: %02.2f%%", port,
100*bf:cell_usage(filter.mac_table)))
end
end
end,
config.timeout *1e9, 'repeating')
)

-- Caches for various cdata pointer objects to avoid boxing in the
-- push() loop
o._cache = {
p = ffi.new("struct packet *[1]"),
iov = ffi.new("struct packet_iovec *[1]"),
mem = ffi.new("uint8_t *[1]")
}
return o
end

-- We only process a single input port for each call of the push()
-- method to reduce the number of nested loops. A better
-- understanding of the JIT compiler is needed to decide whether this
-- is actually a good thing or not. Empirical data suggests it is :)
function bridge:push()
local src_port = self._src_ports[self._port_index]
local l_in = self.input[src_port]
while not empty(l_in) do
local cache = self._cache
local dst_ports = self._dst_ports
local p = cache.p
local iov = cache.iov
local mem = cache.mem
local filters = self._filters
local eth_dst = self._eth_dst
local bf = self._bf
p[0] = receive(l_in)

-- Create a storage item from the destination MAC address
-- for matching with the source addresses learned on the
-- outbound ports, unless it is a multicast address.
iov[0] = p[0].iovecs[0]
mem[0] = iov[0].buffer.pointer + iov[0].offset
local is_mcast = ethernet:is_mcast(mem[0])
if not is_mcast then
bf:store_value(mem, 6, eth_dst)
end

-- Store the source MAC address in the active and shadow
-- Bloom filters.
local filter = filters[src_port]
local mac_address = filter.mac_address
mem[0] = mem[0] + 6
bf:store_value(mem, 6, mac_address, filter.mac_table)
bf:store_item(mac_address, filter.mac_shadow)

local ports = dst_ports[src_port]
local copy = false
local j = 1
while ports[j] do
local dst_port = ports[j]
if is_mcast or bf:check_item(eth_dst, filters[dst_port].mac_table) then
if not copy then
transmit(self.output[dst_port], p[0])
copy = true
else
transmit(self.output[dst_port], cow_clone(p[0]))
end
end
j = j + 1
end
if not copy then
-- The source MAC address is unknown, flood the packet to
-- all ports
local output = self.output
transmit(output[ports[1]], p[0])
local j = 2
while ports[j] do
transmit(output[ports[j]], cow_clone(p[0]))
j = j + 1
end
end
end -- of while not empty(l_in)
if self._port_index == self._nsrc_ports then
self._port_index = 1
else
self._port_index = self._port_index + 1
end
end

return bridge
13 changes: 9 additions & 4 deletions src/core/buffer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function new_buffer ()
local b = lib.malloc("struct buffer")
b.pointer, b.physical, b.size = pointer, physical, buffersize
b.origin.type = C.BUFFER_ORIGIN_UNKNOWN
b.refcount = 1;
return b
end

Expand All @@ -43,11 +44,15 @@ local return_virtio_buffer = net_device.VirtioNetDevice.return_virtio_buffer

-- Free a buffer that is no longer in use.
function free (b)
if b.origin.type == C.BUFFER_ORIGIN_VIRTIO then
local dev = virtio_devices[b.origin.info.virtio.device_id]
return_virtio_buffer(dev, b)
if b.refcount > 1 then
b.refcount = b.refcount - 1
else
freelist.add(buffers, b)
if b.origin.type == C.BUFFER_ORIGIN_VIRTIO then
local dev = virtio_devices[b.origin.info.virtio.device_id]
return_virtio_buffer(dev, b)
else
freelist.add(buffers, b)
end
end
end

Expand Down
1 change: 1 addition & 0 deletions src/core/packet.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ struct buffer {
uint64_t physical; // stable physical address
uint32_t size; // how many bytes in the buffer?
struct buffer_origin origin;
uint16_t refcount; // Counter for references from packets
};

// A packet_iovec describes a portion of a buffer.
Expand Down
Loading

0 comments on commit 7e311e4

Please sign in to comment.