Commit bc6aaa14 authored by David S. Miller's avatar David S. Miller

Merge branch 'rss_hash'

Amir Vadai says:

====================
ethtool, net/mlx4_en: RSS hash function selection

This patchset by Eyal adds support in set/get of RSS hash function. Current
supported functions are Toeplitz and XOR. The API is design to enable adding
new hash functions without breaking backward compatibility.
Userspace patch will be sent after API is available in kernel.

The patchset was applied and tested over commit cd4c910e ("netpoll: delete
defconfig references to obsolete NETPOLL_TRAP")
====================
Signed-off-by: default avatarDavid S. Miller <davem@davemloft.net>
parents 18b5427a 947cbb0a
...@@ -511,7 +511,8 @@ static u32 xgbe_get_rxfh_indir_size(struct net_device *netdev) ...@@ -511,7 +511,8 @@ static u32 xgbe_get_rxfh_indir_size(struct net_device *netdev)
return ARRAY_SIZE(pdata->rss_table); return ARRAY_SIZE(pdata->rss_table);
} }
static int xgbe_get_rxfh(struct net_device *netdev, u32 *indir, u8 *key) static int xgbe_get_rxfh(struct net_device *netdev, u32 *indir, u8 *key,
u8 *hfunc)
{ {
struct xgbe_prv_data *pdata = netdev_priv(netdev); struct xgbe_prv_data *pdata = netdev_priv(netdev);
unsigned int i; unsigned int i;
...@@ -525,16 +526,22 @@ static int xgbe_get_rxfh(struct net_device *netdev, u32 *indir, u8 *key) ...@@ -525,16 +526,22 @@ static int xgbe_get_rxfh(struct net_device *netdev, u32 *indir, u8 *key)
if (key) if (key)
memcpy(key, pdata->rss_key, sizeof(pdata->rss_key)); memcpy(key, pdata->rss_key, sizeof(pdata->rss_key));
if (hfunc)
*hfunc = ETH_RSS_HASH_TOP;
return 0; return 0;
} }
static int xgbe_set_rxfh(struct net_device *netdev, const u32 *indir, static int xgbe_set_rxfh(struct net_device *netdev, const u32 *indir,
const u8 *key) const u8 *key, const u8 hfunc)
{ {
struct xgbe_prv_data *pdata = netdev_priv(netdev); struct xgbe_prv_data *pdata = netdev_priv(netdev);
struct xgbe_hw_if *hw_if = &pdata->hw_if; struct xgbe_hw_if *hw_if = &pdata->hw_if;
unsigned int ret; unsigned int ret;
if (hfunc != ETH_RSS_HASH_NO_CHANGE && hfunc != ETH_RSS_HASH_TOP)
return -EOPNOTSUPP;
if (indir) { if (indir) {
ret = hw_if->set_rss_lookup_table(pdata, indir); ret = hw_if->set_rss_lookup_table(pdata, indir);
if (ret) if (ret)
......
...@@ -3358,12 +3358,18 @@ static u32 bnx2x_get_rxfh_indir_size(struct net_device *dev) ...@@ -3358,12 +3358,18 @@ static u32 bnx2x_get_rxfh_indir_size(struct net_device *dev)
return T_ETH_INDIRECTION_TABLE_SIZE; return T_ETH_INDIRECTION_TABLE_SIZE;
} }
static int bnx2x_get_rxfh(struct net_device *dev, u32 *indir, u8 *key) static int bnx2x_get_rxfh(struct net_device *dev, u32 *indir, u8 *key,
u8 *hfunc)
{ {
struct bnx2x *bp = netdev_priv(dev); struct bnx2x *bp = netdev_priv(dev);
u8 ind_table[T_ETH_INDIRECTION_TABLE_SIZE] = {0}; u8 ind_table[T_ETH_INDIRECTION_TABLE_SIZE] = {0};
size_t i; size_t i;
if (hfunc)
*hfunc = ETH_RSS_HASH_TOP;
if (!indir)
return 0;
/* Get the current configuration of the RSS indirection table */ /* Get the current configuration of the RSS indirection table */
bnx2x_get_rss_ind_table(&bp->rss_conf_obj, ind_table); bnx2x_get_rss_ind_table(&bp->rss_conf_obj, ind_table);
...@@ -3383,11 +3389,21 @@ static int bnx2x_get_rxfh(struct net_device *dev, u32 *indir, u8 *key) ...@@ -3383,11 +3389,21 @@ static int bnx2x_get_rxfh(struct net_device *dev, u32 *indir, u8 *key)
} }
static int bnx2x_set_rxfh(struct net_device *dev, const u32 *indir, static int bnx2x_set_rxfh(struct net_device *dev, const u32 *indir,
const u8 *key) const u8 *key, const u8 hfunc)
{ {
struct bnx2x *bp = netdev_priv(dev); struct bnx2x *bp = netdev_priv(dev);
size_t i; size_t i;
/* We require at least one supported parameter to be changed and no
* change in any of the unsupported parameters
*/
if (key ||
(hfunc != ETH_RSS_HASH_NO_CHANGE && hfunc != ETH_RSS_HASH_TOP))
return -EOPNOTSUPP;
if (!indir)
return 0;
for (i = 0; i < T_ETH_INDIRECTION_TABLE_SIZE; i++) { for (i = 0; i < T_ETH_INDIRECTION_TABLE_SIZE; i++) {
/* /*
* The same as in bnx2x_get_rxfh: we can't use a memcpy() * The same as in bnx2x_get_rxfh: we can't use a memcpy()
......
...@@ -12561,22 +12561,38 @@ static u32 tg3_get_rxfh_indir_size(struct net_device *dev) ...@@ -12561,22 +12561,38 @@ static u32 tg3_get_rxfh_indir_size(struct net_device *dev)
return size; return size;
} }
static int tg3_get_rxfh(struct net_device *dev, u32 *indir, u8 *key) static int tg3_get_rxfh(struct net_device *dev, u32 *indir, u8 *key, u8 *hfunc)
{ {
struct tg3 *tp = netdev_priv(dev); struct tg3 *tp = netdev_priv(dev);
int i; int i;
if (hfunc)
*hfunc = ETH_RSS_HASH_TOP;
if (!indir)
return 0;
for (i = 0; i < TG3_RSS_INDIR_TBL_SIZE; i++) for (i = 0; i < TG3_RSS_INDIR_TBL_SIZE; i++)
indir[i] = tp->rss_ind_tbl[i]; indir[i] = tp->rss_ind_tbl[i];
return 0; return 0;
} }
static int tg3_set_rxfh(struct net_device *dev, const u32 *indir, const u8 *key) static int tg3_set_rxfh(struct net_device *dev, const u32 *indir, const u8 *key,
const u8 hfunc)
{ {
struct tg3 *tp = netdev_priv(dev); struct tg3 *tp = netdev_priv(dev);
size_t i; size_t i;
/* We require at least one supported parameter to be changed and no
* change in any of the unsupported parameters
*/
if (key ||
(hfunc != ETH_RSS_HASH_NO_CHANGE && hfunc != ETH_RSS_HASH_TOP))
return -EOPNOTSUPP;
if (!indir)
return 0;
for (i = 0; i < TG3_RSS_INDIR_TBL_SIZE; i++) for (i = 0; i < TG3_RSS_INDIR_TBL_SIZE; i++)
tp->rss_ind_tbl[i] = indir[i]; tp->rss_ind_tbl[i] = indir[i];
......
...@@ -2923,21 +2923,35 @@ static u32 get_rss_table_size(struct net_device *dev) ...@@ -2923,21 +2923,35 @@ static u32 get_rss_table_size(struct net_device *dev)
return pi->rss_size; return pi->rss_size;
} }
static int get_rss_table(struct net_device *dev, u32 *p, u8 *key) static int get_rss_table(struct net_device *dev, u32 *p, u8 *key, u8 *hfunc)
{ {
const struct port_info *pi = netdev_priv(dev); const struct port_info *pi = netdev_priv(dev);
unsigned int n = pi->rss_size; unsigned int n = pi->rss_size;
if (hfunc)
*hfunc = ETH_RSS_HASH_TOP;
if (!p)
return 0;
while (n--) while (n--)
p[n] = pi->rss[n]; p[n] = pi->rss[n];
return 0; return 0;
} }
static int set_rss_table(struct net_device *dev, const u32 *p, const u8 *key) static int set_rss_table(struct net_device *dev, const u32 *p, const u8 *key,
const u8 hfunc)
{ {
unsigned int i; unsigned int i;
struct port_info *pi = netdev_priv(dev); struct port_info *pi = netdev_priv(dev);
/* We require at least one supported parameter to be changed and no
* change in any of the unsupported parameters
*/
if (key ||
(hfunc != ETH_RSS_HASH_NO_CHANGE && hfunc != ETH_RSS_HASH_TOP))
return -EOPNOTSUPP;
if (!p)
return 0;
for (i = 0; i < pi->rss_size; i++) for (i = 0; i < pi->rss_size; i++)
pi->rss[i] = p[i]; pi->rss[i] = p[i];
if (pi->adapter->flags & FULL_INIT_DONE) if (pi->adapter->flags & FULL_INIT_DONE)
......
...@@ -1171,7 +1171,8 @@ static u32 be_get_rxfh_key_size(struct net_device *netdev) ...@@ -1171,7 +1171,8 @@ static u32 be_get_rxfh_key_size(struct net_device *netdev)
return RSS_HASH_KEY_LEN; return RSS_HASH_KEY_LEN;
} }
static int be_get_rxfh(struct net_device *netdev, u32 *indir, u8 *hkey) static int be_get_rxfh(struct net_device *netdev, u32 *indir, u8 *hkey,
u8 *hfunc)
{ {
struct be_adapter *adapter = netdev_priv(netdev); struct be_adapter *adapter = netdev_priv(netdev);
int i; int i;
...@@ -1185,16 +1186,23 @@ static int be_get_rxfh(struct net_device *netdev, u32 *indir, u8 *hkey) ...@@ -1185,16 +1186,23 @@ static int be_get_rxfh(struct net_device *netdev, u32 *indir, u8 *hkey)
if (hkey) if (hkey)
memcpy(hkey, rss->rss_hkey, RSS_HASH_KEY_LEN); memcpy(hkey, rss->rss_hkey, RSS_HASH_KEY_LEN);
if (hfunc)
*hfunc = ETH_RSS_HASH_TOP;
return 0; return 0;
} }
static int be_set_rxfh(struct net_device *netdev, const u32 *indir, static int be_set_rxfh(struct net_device *netdev, const u32 *indir,
const u8 *hkey) const u8 *hkey, const u8 hfunc)
{ {
int rc = 0, i, j; int rc = 0, i, j;
struct be_adapter *adapter = netdev_priv(netdev); struct be_adapter *adapter = netdev_priv(netdev);
u8 rsstable[RSS_INDIR_TABLE_LEN]; u8 rsstable[RSS_INDIR_TABLE_LEN];
/* We do not allow change in unsupported parameters */
if (hfunc != ETH_RSS_HASH_NO_CHANGE && hfunc != ETH_RSS_HASH_TOP)
return -EOPNOTSUPP;
if (indir) { if (indir) {
struct be_rx_obj *rxo; struct be_rx_obj *rxo;
......
...@@ -916,11 +916,15 @@ static u32 fm10k_get_rssrk_size(struct net_device *netdev) ...@@ -916,11 +916,15 @@ static u32 fm10k_get_rssrk_size(struct net_device *netdev)
return FM10K_RSSRK_SIZE * FM10K_RSSRK_ENTRIES_PER_REG; return FM10K_RSSRK_SIZE * FM10K_RSSRK_ENTRIES_PER_REG;
} }
static int fm10k_get_rssh(struct net_device *netdev, u32 *indir, u8 *key) static int fm10k_get_rssh(struct net_device *netdev, u32 *indir, u8 *key,
u8 *hfunc)
{ {
struct fm10k_intfc *interface = netdev_priv(netdev); struct fm10k_intfc *interface = netdev_priv(netdev);
int i, err; int i, err;
if (hfunc)
*hfunc = ETH_RSS_HASH_TOP;
err = fm10k_get_reta(netdev, indir); err = fm10k_get_reta(netdev, indir);
if (err || !key) if (err || !key)
return err; return err;
...@@ -932,12 +936,16 @@ static int fm10k_get_rssh(struct net_device *netdev, u32 *indir, u8 *key) ...@@ -932,12 +936,16 @@ static int fm10k_get_rssh(struct net_device *netdev, u32 *indir, u8 *key)
} }
static int fm10k_set_rssh(struct net_device *netdev, const u32 *indir, static int fm10k_set_rssh(struct net_device *netdev, const u32 *indir,
const u8 *key) const u8 *key, const u8 hfunc)
{ {
struct fm10k_intfc *interface = netdev_priv(netdev); struct fm10k_intfc *interface = netdev_priv(netdev);
struct fm10k_hw *hw = &interface->hw; struct fm10k_hw *hw = &interface->hw;
int i, err; int i, err;
/* We do not allow change in unsupported parameters */
if (hfunc != ETH_RSS_HASH_NO_CHANGE && hfunc != ETH_RSS_HASH_TOP)
return -EOPNOTSUPP;
err = fm10k_set_reta(netdev, indir); err = fm10k_set_reta(netdev, indir);
if (err || !key) if (err || !key)
return err; return err;
......
...@@ -627,13 +627,19 @@ static u32 i40evf_get_rxfh_indir_size(struct net_device *netdev) ...@@ -627,13 +627,19 @@ static u32 i40evf_get_rxfh_indir_size(struct net_device *netdev)
* *
* Reads the indirection table directly from the hardware. Always returns 0. * Reads the indirection table directly from the hardware. Always returns 0.
**/ **/
static int i40evf_get_rxfh(struct net_device *netdev, u32 *indir, u8 *key) static int i40evf_get_rxfh(struct net_device *netdev, u32 *indir, u8 *key,
u8 *hfunc)
{ {
struct i40evf_adapter *adapter = netdev_priv(netdev); struct i40evf_adapter *adapter = netdev_priv(netdev);
struct i40e_hw *hw = &adapter->hw; struct i40e_hw *hw = &adapter->hw;
u32 hlut_val; u32 hlut_val;
int i, j; int i, j;
if (hfunc)
*hfunc = ETH_RSS_HASH_TOP;
if (!indir)
return 0;
for (i = 0, j = 0; i <= I40E_VFQF_HLUT_MAX_INDEX; i++) { for (i = 0, j = 0; i <= I40E_VFQF_HLUT_MAX_INDEX; i++) {
hlut_val = rd32(hw, I40E_VFQF_HLUT(i)); hlut_val = rd32(hw, I40E_VFQF_HLUT(i));
indir[j++] = hlut_val & 0xff; indir[j++] = hlut_val & 0xff;
...@@ -654,13 +660,20 @@ static int i40evf_get_rxfh(struct net_device *netdev, u32 *indir, u8 *key) ...@@ -654,13 +660,20 @@ static int i40evf_get_rxfh(struct net_device *netdev, u32 *indir, u8 *key)
* returns 0 after programming the table. * returns 0 after programming the table.
**/ **/
static int i40evf_set_rxfh(struct net_device *netdev, const u32 *indir, static int i40evf_set_rxfh(struct net_device *netdev, const u32 *indir,
const u8 *key) const u8 *key, const u8 hfunc)
{ {
struct i40evf_adapter *adapter = netdev_priv(netdev); struct i40evf_adapter *adapter = netdev_priv(netdev);
struct i40e_hw *hw = &adapter->hw; struct i40e_hw *hw = &adapter->hw;
u32 hlut_val; u32 hlut_val;
int i, j; int i, j;
/* We do not allow change in unsupported parameters */
if (key ||
(hfunc != ETH_RSS_HASH_NO_CHANGE && hfunc != ETH_RSS_HASH_TOP))
return -EOPNOTSUPP;
if (!indir)
return 0;
for (i = 0, j = 0; i <= I40E_VFQF_HLUT_MAX_INDEX; i++) { for (i = 0, j = 0; i <= I40E_VFQF_HLUT_MAX_INDEX; i++) {
hlut_val = indir[j++]; hlut_val = indir[j++];
hlut_val |= indir[j++] << 8; hlut_val |= indir[j++] << 8;
......
...@@ -2842,11 +2842,16 @@ static u32 igb_get_rxfh_indir_size(struct net_device *netdev) ...@@ -2842,11 +2842,16 @@ static u32 igb_get_rxfh_indir_size(struct net_device *netdev)
return IGB_RETA_SIZE; return IGB_RETA_SIZE;
} }
static int igb_get_rxfh(struct net_device *netdev, u32 *indir, u8 *key) static int igb_get_rxfh(struct net_device *netdev, u32 *indir, u8 *key,
u8 *hfunc)
{ {
struct igb_adapter *adapter = netdev_priv(netdev); struct igb_adapter *adapter = netdev_priv(netdev);
int i; int i;
if (hfunc)
*hfunc = ETH_RSS_HASH_TOP;
if (!indir)
return 0;
for (i = 0; i < IGB_RETA_SIZE; i++) for (i = 0; i < IGB_RETA_SIZE; i++)
indir[i] = adapter->rss_indir_tbl[i]; indir[i] = adapter->rss_indir_tbl[i];
...@@ -2889,13 +2894,20 @@ void igb_write_rss_indir_tbl(struct igb_adapter *adapter) ...@@ -2889,13 +2894,20 @@ void igb_write_rss_indir_tbl(struct igb_adapter *adapter)
} }
static int igb_set_rxfh(struct net_device *netdev, const u32 *indir, static int igb_set_rxfh(struct net_device *netdev, const u32 *indir,
const u8 *key) const u8 *key, const u8 hfunc)
{ {
struct igb_adapter *adapter = netdev_priv(netdev); struct igb_adapter *adapter = netdev_priv(netdev);
struct e1000_hw *hw = &adapter->hw; struct e1000_hw *hw = &adapter->hw;
int i; int i;
u32 num_queues; u32 num_queues;
/* We do not allow change in unsupported parameters */
if (key ||
(hfunc != ETH_RSS_HASH_NO_CHANGE && hfunc != ETH_RSS_HASH_TOP))
return -EOPNOTSUPP;
if (!indir)
return 0;
num_queues = adapter->rss_queues; num_queues = adapter->rss_queues;
switch (hw->mac.type) { switch (hw->mac.type) {
......
...@@ -978,7 +978,29 @@ static u32 mlx4_en_get_rxfh_key_size(struct net_device *netdev) ...@@ -978,7 +978,29 @@ static u32 mlx4_en_get_rxfh_key_size(struct net_device *netdev)
return MLX4_EN_RSS_KEY_SIZE; return MLX4_EN_RSS_KEY_SIZE;
} }
static int mlx4_en_get_rxfh(struct net_device *dev, u32 *ring_index, u8 *key) static int mlx4_en_check_rxfh_func(struct net_device *dev, u8 hfunc)
{
struct mlx4_en_priv *priv = netdev_priv(dev);
/* check if requested function is supported by the device */
if ((hfunc == ETH_RSS_HASH_TOP &&
!(priv->mdev->dev->caps.flags2 & MLX4_DEV_CAP_FLAG2_RSS_TOP)) ||
(hfunc == ETH_RSS_HASH_XOR &&
!(priv->mdev->dev->caps.flags2 & MLX4_DEV_CAP_FLAG2_RSS_XOR)))
return -EINVAL;
priv->rss_hash_fn = hfunc;
if (hfunc == ETH_RSS_HASH_TOP && !(dev->features & NETIF_F_RXHASH))
en_warn(priv,
"Toeplitz hash function should be used in conjunction with RX hashing for optimal performance\n");
if (hfunc == ETH_RSS_HASH_XOR && (dev->features & NETIF_F_RXHASH))
en_warn(priv,
"Enabling both XOR Hash function and RX Hashing can limit RPS functionality\n");
return 0;
}
static int mlx4_en_get_rxfh(struct net_device *dev, u32 *ring_index, u8 *key,
u8 *hfunc)
{ {
struct mlx4_en_priv *priv = netdev_priv(dev); struct mlx4_en_priv *priv = netdev_priv(dev);
struct mlx4_en_rss_map *rss_map = &priv->rss_map; struct mlx4_en_rss_map *rss_map = &priv->rss_map;
...@@ -990,16 +1012,20 @@ static int mlx4_en_get_rxfh(struct net_device *dev, u32 *ring_index, u8 *key) ...@@ -990,16 +1012,20 @@ static int mlx4_en_get_rxfh(struct net_device *dev, u32 *ring_index, u8 *key)
rss_rings = 1 << ilog2(rss_rings); rss_rings = 1 << ilog2(rss_rings);
while (n--) { while (n--) {
if (!ring_index)
break;
ring_index[n] = rss_map->qps[n % rss_rings].qpn - ring_index[n] = rss_map->qps[n % rss_rings].qpn -
rss_map->base_qpn; rss_map->base_qpn;
} }
if (key) if (key)
memcpy(key, priv->rss_key, MLX4_EN_RSS_KEY_SIZE); memcpy(key, priv->rss_key, MLX4_EN_RSS_KEY_SIZE);
if (hfunc)
*hfunc = priv->rss_hash_fn;
return err; return err;
} }
static int mlx4_en_set_rxfh(struct net_device *dev, const u32 *ring_index, static int mlx4_en_set_rxfh(struct net_device *dev, const u32 *ring_index,
const u8 *key) const u8 *key, const u8 hfunc)
{ {
struct mlx4_en_priv *priv = netdev_priv(dev); struct mlx4_en_priv *priv = netdev_priv(dev);
struct mlx4_en_dev *mdev = priv->mdev; struct mlx4_en_dev *mdev = priv->mdev;
...@@ -1028,6 +1054,12 @@ static int mlx4_en_set_rxfh(struct net_device *dev, const u32 *ring_index, ...@@ -1028,6 +1054,12 @@ static int mlx4_en_set_rxfh(struct net_device *dev, const u32 *ring_index,
if (!is_power_of_2(rss_rings)) if (!is_power_of_2(rss_rings))
return -EINVAL; return -EINVAL;
if (hfunc != ETH_RSS_HASH_NO_CHANGE) {
err = mlx4_en_check_rxfh_func(dev, hfunc);
if (err)
return err;
}
mutex_lock(&mdev->state_lock); mutex_lock(&mdev->state_lock);
if (priv->port_up) { if (priv->port_up) {
port_up = 1; port_up = 1;
...@@ -1038,6 +1070,7 @@ static int mlx4_en_set_rxfh(struct net_device *dev, const u32 *ring_index, ...@@ -1038,6 +1070,7 @@ static int mlx4_en_set_rxfh(struct net_device *dev, const u32 *ring_index,
priv->prof->rss_rings = rss_rings; priv->prof->rss_rings = rss_rings;
if (key) if (key)
memcpy(priv->rss_key, key, MLX4_EN_RSS_KEY_SIZE); memcpy(priv->rss_key, key, MLX4_EN_RSS_KEY_SIZE);
if (port_up) { if (port_up) {
err = mlx4_en_start_port(dev); err = mlx4_en_start_port(dev);
if (err) if (err)
......
...@@ -2608,6 +2608,17 @@ int mlx4_en_init_netdev(struct mlx4_en_dev *mdev, int port, ...@@ -2608,6 +2608,17 @@ int mlx4_en_init_netdev(struct mlx4_en_dev *mdev, int port,
if (mdev->dev->caps.steering_mode != MLX4_STEERING_MODE_A0) if (mdev->dev->caps.steering_mode != MLX4_STEERING_MODE_A0)
dev->priv_flags |= IFF_UNICAST_FLT; dev->priv_flags |= IFF_UNICAST_FLT;
/* Setting a default hash function value */
if (mdev->dev->caps.flags2 & MLX4_DEV_CAP_FLAG2_RSS_TOP) {
priv->rss_hash_fn = ETH_RSS_HASH_TOP;
} else if (mdev->dev->caps.flags2 & MLX4_DEV_CAP_FLAG2_RSS_XOR) {
priv->rss_hash_fn = ETH_RSS_HASH_XOR;
} else {
en_warn(priv,
"No RSS hash capabilities exposed, using Toeplitz\n");
priv->rss_hash_fn = ETH_RSS_HASH_TOP;
}
mdev->pndev[port] = dev; mdev->pndev[port] = dev;
netif_carrier_off(dev); netif_carrier_off(dev);
......
...@@ -1223,7 +1223,19 @@ int mlx4_en_config_rss_steer(struct mlx4_en_priv *priv) ...@@ -1223,7 +1223,19 @@ int mlx4_en_config_rss_steer(struct mlx4_en_priv *priv)
rss_context->flags = rss_mask; rss_context->flags = rss_mask;
rss_context->hash_fn = MLX4_RSS_HASH_TOP; rss_context->hash_fn = MLX4_RSS_HASH_TOP;
memcpy(rss_context->rss_key, priv->rss_key, MLX4_EN_RSS_KEY_SIZE); if (priv->rss_hash_fn == ETH_RSS_HASH_XOR) {
rss_context->hash_fn = MLX4_RSS_HASH_XOR;
} else if (priv->rss_hash_fn == ETH_RSS_HASH_TOP) {
rss_context->hash_fn = MLX4_RSS_HASH_TOP;
memcpy(rss_context->rss_key, priv->rss_key,
MLX4_EN_RSS_KEY_SIZE);
netdev_rss_key_fill(rss_context->rss_key,
MLX4_EN_RSS_KEY_SIZE);
} else {
en_err(priv, "Unknown RSS hash function requested\n");
err = -EINVAL;
goto indir_err;
}
err = mlx4_qp_to_ready(mdev->dev, &priv->res.mtt, &context, err = mlx4_qp_to_ready(mdev->dev, &priv->res.mtt, &context,
&rss_map->indir_qp, &rss_map->indir_state); &rss_map->indir_qp, &rss_map->indir_state);
if (err) if (err)
......
...@@ -376,7 +376,6 @@ struct mlx4_en_port_profile { ...@@ -376,7 +376,6 @@ struct mlx4_en_port_profile {
}; };
struct mlx4_en_profile { struct mlx4_en_profile {
int rss_xor;
int udp_rss; int udp_rss;
u8 rss_mask; u8 rss_mask;
u32 active_ports; u32 active_ports;
...@@ -619,6 +618,7 @@ struct mlx4_en_priv { ...@@ -619,6 +618,7 @@ struct mlx4_en_priv {
u32 pflags; u32 pflags;
u8 rss_key[MLX4_EN_RSS_KEY_SIZE]; u8 rss_key[MLX4_EN_RSS_KEY_SIZE];
u8 rss_hash_fn;
}; };
enum mlx4_en_wol { enum mlx4_en_wol {
......
...@@ -1086,19 +1086,29 @@ static u32 efx_ethtool_get_rxfh_indir_size(struct net_device *net_dev) ...@@ -1086,19 +1086,29 @@ static u32 efx_ethtool_get_rxfh_indir_size(struct net_device *net_dev)
0 : ARRAY_SIZE(efx->rx_indir_table)); 0 : ARRAY_SIZE(efx->rx_indir_table));
} }
static int efx_ethtool_get_rxfh(struct net_device *net_dev, u32 *indir, u8 *key) static int efx_ethtool_get_rxfh(struct net_device *net_dev, u32 *indir, u8 *key,
u8 *hfunc)
{ {
struct efx_nic *efx = netdev_priv(net_dev); struct efx_nic *efx = netdev_priv(net_dev);
memcpy(indir, efx->rx_indir_table, sizeof(efx->rx_indir_table)); if (hfunc)
*hfunc = ETH_RSS_HASH_TOP;
if (indir)
memcpy(indir, efx->rx_indir_table, sizeof(efx->rx_indir_table));
return 0; return 0;
} }
static int efx_ethtool_set_rxfh(struct net_device *net_dev, static int efx_ethtool_set_rxfh(struct net_device *net_dev, const u32 *indir,
const u32 *indir, const u8 *key) const u8 *key, const u8 hfunc)
{ {
struct efx_nic *efx = netdev_priv(net_dev); struct efx_nic *efx = netdev_priv(net_dev);
/* We do not allow change in unsupported parameters */
if (key ||
(hfunc != ETH_RSS_HASH_NO_CHANGE && hfunc != ETH_RSS_HASH_TOP))
return -EOPNOTSUPP;
if (!indir)
return 0;
memcpy(efx->rx_indir_table, indir, sizeof(efx->rx_indir_table)); memcpy(efx->rx_indir_table, indir, sizeof(efx->rx_indir_table));
efx->type->rx_push_rss_config(efx); efx->type->rx_push_rss_config(efx);
return 0; return 0;
......
...@@ -583,12 +583,16 @@ vmxnet3_get_rss_indir_size(struct net_device *netdev) ...@@ -583,12 +583,16 @@ vmxnet3_get_rss_indir_size(struct net_device *netdev)
} }
static int static int
vmxnet3_get_rss(struct net_device *netdev, u32 *p, u8 *key) vmxnet3_get_rss(struct net_device *netdev, u32 *p, u8 *key, u8 *hfunc)
{ {
struct vmxnet3_adapter *adapter = netdev_priv(netdev); struct vmxnet3_adapter *adapter = netdev_priv(netdev);
struct UPT1_RSSConf *rssConf = adapter->rss_conf; struct UPT1_RSSConf *rssConf = adapter->rss_conf;
unsigned int n = rssConf->indTableSize; unsigned int n = rssConf->indTableSize;
if (hfunc)
*hfunc = ETH_RSS_HASH_TOP;
if (!p)
return 0;
while (n--) while (n--)
p[n] = rssConf->indTable[n]; p[n] = rssConf->indTable[n];
return 0; return 0;
...@@ -596,13 +600,20 @@ vmxnet3_get_rss(struct net_device *netdev, u32 *p, u8 *key) ...@@ -596,13 +600,20 @@ vmxnet3_get_rss(struct net_device *netdev, u32 *p, u8 *key)
} }
static int static int
vmxnet3_set_rss(struct net_device *netdev, const u32 *p, const u8 *key) vmxnet3_set_rss(struct net_device *netdev, const u32 *p, const u8 *key,
const u8 hfunc)
{ {
unsigned int i; unsigned int i;
unsigned long flags; unsigned long flags;
struct vmxnet3_adapter *adapter = netdev_priv(netdev); struct vmxnet3_adapter *adapter = netdev_priv(netdev);
struct UPT1_RSSConf *rssConf = adapter->rss_conf; struct UPT1_RSSConf *rssConf = adapter->rss_conf;
/* We do not allow change in unsupported parameters */
if (key ||
(hfunc != ETH_RSS_HASH_NO_CHANGE && hfunc != ETH_RSS_HASH_TOP))
return -EOPNOTSUPP;
if (!p)
return 0;
for (i = 0; i < rssConf->indTableSize; i++) for (i = 0; i < rssConf->indTableSize; i++)
rssConf->indTable[i] = p[i]; rssConf->indTable[i] = p[i];
......
...@@ -59,6 +59,26 @@ enum ethtool_phys_id_state { ...@@ -59,6 +59,26 @@ enum ethtool_phys_id_state {
ETHTOOL_ID_OFF ETHTOOL_ID_OFF
}; };
enum {
ETH_RSS_HASH_TOP_BIT, /* Configurable RSS hash function - Toeplitz */
ETH_RSS_HASH_XOR_BIT, /* Configurable RSS hash function - Xor */
/*
* Add your fresh new hash function bits above and remember to update
* rss_hash_func_strings[] in ethtool.c
*/
ETH_RSS_HASH_FUNCS_COUNT
};
#define __ETH_RSS_HASH_BIT(bit) ((u32)1 << (bit))
#define __ETH_RSS_HASH(name) __ETH_RSS_HASH_BIT(ETH_RSS_HASH_##name##_BIT)
#define ETH_RSS_HASH_TOP __ETH_RSS_HASH(TOP)
#define ETH_RSS_HASH_XOR __ETH_RSS_HASH(XOR)
#define ETH_RSS_HASH_UNKNOWN 0
#define ETH_RSS_HASH_NO_CHANGE 0
struct net_device; struct net_device;
/* Some generic methods drivers may use in their ethtool_ops */ /* Some generic methods drivers may use in their ethtool_ops */
...@@ -158,17 +178,14 @@ static inline u32 ethtool_rxfh_indir_default(u32 index, u32 n_rx_rings) ...@@ -158,17 +178,14 @@ static inline u32 ethtool_rxfh_indir_default(u32 index, u32 n_rx_rings)
* Returns zero if not supported for this specific device. * Returns zero if not supported for this specific device.
* @get_rxfh_indir_size: Get the size of the RX flow hash indirection table. * @get_rxfh_indir_size: Get the size of the RX flow hash indirection table.
* Returns zero if not supported for this specific device. * Returns zero if not supported for this specific device.
* @get_rxfh: Get the contents of the RX flow hash indirection table and hash * @get_rxfh: Get the contents of the RX flow hash indirection table, hash key
* key. * and/or hash function.
* Will only be called if one or both of @get_rxfh_indir_size and
* @get_rxfh_key_size are implemented and return non-zero.
* Returns a negative error code or zero.
* @set_rxfh: Set the contents of the RX flow hash indirection table and/or
* hash key. In case only the indirection table or hash key is to be
* changed, the other argument will be %NULL.
* Will only be called if one or both of @get_rxfh_indir_size and
* @get_rxfh_key_size are implemented and return non-zero.
* Returns a negative error code or zero. * Returns a negative error code or zero.
* @set_rxfh: Set the contents of the RX flow hash indirection table, hash
* key, and/or hash function. Arguments which are set to %NULL or zero
* will remain unchanged.
* Returns a negative error code or zero. An error code must be returned
* if at least one unsupported change was requested.
* @get_channels: Get number of channels. * @get_channels: Get number of channels.
* @set_channels: Set number of channels. Returns a negative error code or * @set_channels: Set number of channels. Returns a negative error code or
* zero. * zero.
...@@ -241,9 +258,10 @@ struct ethtool_ops { ...@@ -241,9 +258,10 @@ struct ethtool_ops {
int (*reset)(struct net_device *, u32 *); int (*reset)(struct net_device *, u32 *);
u32 (*get_rxfh_key_size)(struct net_device *); u32 (*get_rxfh_key_size)(struct net_device *);
u32 (*get_rxfh_indir_size)(struct net_device *); u32 (*get_rxfh_indir_size)(struct net_device *);
int (*get_rxfh)(struct net_device *, u32 *indir, u8 *key); int (*get_rxfh)(struct net_device *, u32 *indir, u8 *key,
u8 *hfunc);
int (*set_rxfh)(struct net_device *, const u32 *indir, int (*set_rxfh)(struct net_device *, const u32 *indir,
const u8 *key); const u8 *key, const u8 hfunc);
void (*get_channels)(struct net_device *, struct ethtool_channels *); void (*get_channels)(struct net_device *, struct ethtool_channels *);
int (*set_channels)(struct net_device *, struct ethtool_channels *); int (*set_channels)(struct net_device *, struct ethtool_channels *);
int (*get_dump_flag)(struct net_device *, struct ethtool_dump *); int (*get_dump_flag)(struct net_device *, struct ethtool_dump *);
......
...@@ -534,6 +534,7 @@ struct ethtool_pauseparam { ...@@ -534,6 +534,7 @@ struct ethtool_pauseparam {
* @ETH_SS_NTUPLE_FILTERS: Previously used with %ETHTOOL_GRXNTUPLE; * @ETH_SS_NTUPLE_FILTERS: Previously used with %ETHTOOL_GRXNTUPLE;
* now deprecated * now deprecated
* @ETH_SS_FEATURES: Device feature names * @ETH_SS_FEATURES: Device feature names
* @ETH_SS_RSS_HASH_FUNCS: RSS hush function names
*/ */
enum ethtool_stringset { enum ethtool_stringset {
ETH_SS_TEST = 0, ETH_SS_TEST = 0,
...@@ -541,6 +542,7 @@ enum ethtool_stringset { ...@@ -541,6 +542,7 @@ enum ethtool_stringset {
ETH_SS_PRIV_FLAGS, ETH_SS_PRIV_FLAGS,
ETH_SS_NTUPLE_FILTERS, ETH_SS_NTUPLE_FILTERS,
ETH_SS_FEATURES, ETH_SS_FEATURES,
ETH_SS_RSS_HASH_FUNCS,
}; };
/** /**
...@@ -884,6 +886,8 @@ struct ethtool_rxfh_indir { ...@@ -884,6 +886,8 @@ struct ethtool_rxfh_indir {
* @key_size: On entry, the array size of the user buffer for the hash key, * @key_size: On entry, the array size of the user buffer for the hash key,
* which may be zero. On return from %ETHTOOL_GRSSH, the size of the * which may be zero. On return from %ETHTOOL_GRSSH, the size of the
* hardware hash key. * hardware hash key.
* @hfunc: Defines the current RSS hash function used by HW (or to be set to).
* Valid values are one of the %ETH_RSS_HASH_*.
* @rsvd: Reserved for future extensions. * @rsvd: Reserved for future extensions.
* @rss_config: RX ring/queue index for each hash value i.e., indirection table * @rss_config: RX ring/queue index for each hash value i.e., indirection table
* of @indir_size __u32 elements, followed by hash key of @key_size * of @indir_size __u32 elements, followed by hash key of @key_size
...@@ -893,14 +897,16 @@ struct ethtool_rxfh_indir { ...@@ -893,14 +897,16 @@ struct ethtool_rxfh_indir {
* size should be returned. For %ETHTOOL_SRSSH, an @indir_size of * size should be returned. For %ETHTOOL_SRSSH, an @indir_size of
* %ETH_RXFH_INDIR_NO_CHANGE means that indir table setting is not requested * %ETH_RXFH_INDIR_NO_CHANGE means that indir table setting is not requested
* and a @indir_size of zero means the indir table should be reset to default * and a @indir_size of zero means the indir table should be reset to default
* values. * values. An hfunc of zero means that hash function setting is not requested.
*/ */
struct ethtool_rxfh { struct ethtool_rxfh {
__u32 cmd; __u32 cmd;
__u32 rss_context; __u32 rss_context;
__u32 indir_size; __u32 indir_size;
__u32 key_size; __u32 key_size;
__u32 rsvd[2]; __u8 hfunc;
__u8 rsvd8[3];
__u32 rsvd32;
__u32 rss_config[0]; __u32 rss_config[0];
}; };
#define ETH_RXFH_INDIR_NO_CHANGE 0xffffffff #define ETH_RXFH_INDIR_NO_CHANGE 0xffffffff
......
...@@ -100,6 +100,12 @@ static const char netdev_features_strings[NETDEV_FEATURE_COUNT][ETH_GSTRING_LEN] ...@@ -100,6 +100,12 @@ static const char netdev_features_strings[NETDEV_FEATURE_COUNT][ETH_GSTRING_LEN]
[NETIF_F_BUSY_POLL_BIT] = "busy-poll", [NETIF_F_BUSY_POLL_BIT] = "busy-poll",
}; };
static const char
rss_hash_func_strings[ETH_RSS_HASH_FUNCS_COUNT][ETH_GSTRING_LEN] = {
[ETH_RSS_HASH_TOP_BIT] = "toeplitz",
[ETH_RSS_HASH_XOR_BIT] = "xor",
};
static int ethtool_get_features(struct net_device *dev, void __user *useraddr) static int ethtool_get_features(struct net_device *dev, void __user *useraddr)
{ {
struct ethtool_gfeatures cmd = { struct ethtool_gfeatures cmd = {
...@@ -185,6 +191,9 @@ static int __ethtool_get_sset_count(struct net_device *dev, int sset) ...@@ -185,6 +191,9 @@ static int __ethtool_get_sset_count(struct net_device *dev, int sset)
if (sset == ETH_SS_FEATURES) if (sset == ETH_SS_FEATURES)
return ARRAY_SIZE(netdev_features_strings); return ARRAY_SIZE(netdev_features_strings);
if (sset == ETH_SS_RSS_HASH_FUNCS)
return ARRAY_SIZE(rss_hash_func_strings);
if (ops->get_sset_count && ops->get_strings) if (ops->get_sset_count && ops->get_strings)
return ops->get_sset_count(dev, sset); return ops->get_sset_count(dev, sset);
else else
...@@ -199,6 +208,9 @@ static void __ethtool_get_strings(struct net_device *dev, ...@@ -199,6 +208,9 @@ static void __ethtool_get_strings(struct net_device *dev,
if (stringset == ETH_SS_FEATURES) if (stringset == ETH_SS_FEATURES)
memcpy(data, netdev_features_strings, memcpy(data, netdev_features_strings,
sizeof(netdev_features_strings)); sizeof(netdev_features_strings));
else if (stringset == ETH_SS_RSS_HASH_FUNCS)
memcpy(data, rss_hash_func_strings,
sizeof(rss_hash_func_strings));
else else
/* ops->get_strings is valid because checked earlier */ /* ops->get_strings is valid because checked earlier */
ops->get_strings(dev, stringset, data); ops->get_strings(dev, stringset, data);
...@@ -618,7 +630,7 @@ static noinline_for_stack int ethtool_get_rxfh_indir(struct net_device *dev, ...@@ -618,7 +630,7 @@ static noinline_for_stack int ethtool_get_rxfh_indir(struct net_device *dev,
if (!indir) if (!indir)
return -ENOMEM; return -ENOMEM;
ret = dev->ethtool_ops->get_rxfh(dev, indir, NULL); ret = dev->ethtool_ops->get_rxfh(dev, indir, NULL, NULL);
if (ret) if (ret)
goto out; goto out;
...@@ -679,7 +691,7 @@ static noinline_for_stack int ethtool_set_rxfh_indir(struct net_device *dev, ...@@ -679,7 +691,7 @@ static noinline_for_stack int ethtool_set_rxfh_indir(struct net_device *dev,
goto out; goto out;
} }
ret = ops->set_rxfh(dev, indir, NULL); ret = ops->set_rxfh(dev, indir, NULL, ETH_RSS_HASH_NO_CHANGE);
out: out:
kfree(indir); kfree(indir);
...@@ -697,12 +709,11 @@ static noinline_for_stack int ethtool_get_rxfh(struct net_device *dev, ...@@ -697,12 +709,11 @@ static noinline_for_stack int ethtool_get_rxfh(struct net_device *dev,
u32 total_size; u32 total_size;
u32 indir_bytes; u32 indir_bytes;
u32 *indir = NULL; u32 *indir = NULL;
u8 dev_hfunc = 0;
u8 *hkey = NULL; u8 *hkey = NULL;
u8 *rss_config; u8 *rss_config;
if (!(dev->ethtool_ops->get_rxfh_indir_size || if (!ops->get_rxfh)
dev->ethtool_ops->get_rxfh_key_size) ||
!dev->ethtool_ops->get_rxfh)
return -EOPNOTSUPP; return -EOPNOTSUPP;
if (ops->get_rxfh_indir_size) if (ops->get_rxfh_indir_size)
...@@ -710,16 +721,14 @@ static noinline_for_stack int ethtool_get_rxfh(struct net_device *dev, ...@@ -710,16 +721,14 @@ static noinline_for_stack int ethtool_get_rxfh(struct net_device *dev,
if (ops->get_rxfh_key_size) if (ops->get_rxfh_key_size)
dev_key_size = ops->get_rxfh_key_size(dev); dev_key_size = ops->get_rxfh_key_size(dev);
if ((dev_key_size + dev_indir_size) == 0)
return -EOPNOTSUPP;
if (copy_from_user(&rxfh, useraddr, sizeof(rxfh))) if (copy_from_user(&rxfh, useraddr, sizeof(rxfh)))
return -EFAULT; return -EFAULT;
user_indir_size = rxfh.indir_size; user_indir_size = rxfh.indir_size;
user_key_size = rxfh.key_size; user_key_size = rxfh.key_size;
/* Check that reserved fields are 0 for now */ /* Check that reserved fields are 0 for now */
if (rxfh.rss_context || rxfh.rsvd[0] || rxfh.rsvd[1]) if (rxfh.rss_context || rxfh.rsvd8[0] || rxfh.rsvd8[1] ||
rxfh.rsvd8[2] || rxfh.rsvd32)
return -EINVAL; return -EINVAL;
rxfh.indir_size = dev_indir_size; rxfh.indir_size = dev_indir_size;
...@@ -727,13 +736,6 @@ static noinline_for_stack int ethtool_get_rxfh(struct net_device *dev, ...@@ -727,13 +736,6 @@ static noinline_for_stack int ethtool_get_rxfh(struct net_device *dev,
if (copy_to_user(useraddr, &rxfh, sizeof(rxfh))) if (copy_to_user(useraddr, &rxfh, sizeof(rxfh)))
return -EFAULT; return -EFAULT;
/* If the user buffer size is 0, this is just a query for the
* device table size and key size. Otherwise, if the User size is
* not equal to device table size or key size it's an error.
*/
if (!user_indir_size && !user_key_size)
return 0;
if ((user_indir_size && (user_indir_size != dev_indir_size)) || if ((user_indir_size && (user_indir_size != dev_indir_size)) ||
(user_key_size && (user_key_size != dev_key_size))) (user_key_size && (user_key_size != dev_key_size)))
return -EINVAL; return -EINVAL;
...@@ -750,14 +752,19 @@ static noinline_for_stack int ethtool_get_rxfh(struct net_device *dev, ...@@ -750,14 +752,19 @@ static noinline_for_stack int ethtool_get_rxfh(struct net_device *dev,
if (user_key_size) if (user_key_size)
hkey = rss_config + indir_bytes; hkey = rss_config + indir_bytes;
ret = dev->ethtool_ops->get_rxfh(dev, indir, hkey); ret = dev->ethtool_ops->get_rxfh(dev, indir, hkey, &dev_hfunc);
if (!ret) { if (ret)
if (copy_to_user(useraddr + goto out;
offsetof(struct ethtool_rxfh, rss_config[0]),
rss_config, total_size))
ret = -EFAULT;
}
if (copy_to_user(useraddr + offsetof(struct ethtool_rxfh, hfunc),
&dev_hfunc, sizeof(rxfh.hfunc))) {
ret = -EFAULT;
} else if (copy_to_user(useraddr +
offsetof(struct ethtool_rxfh, rss_config[0]),
rss_config, total_size)) {
ret = -EFAULT;
}
out:
kfree(rss_config); kfree(rss_config);
return ret; return ret;
...@@ -776,33 +783,31 @@ static noinline_for_stack int ethtool_set_rxfh(struct net_device *dev, ...@@ -776,33 +783,31 @@ static noinline_for_stack int ethtool_set_rxfh(struct net_device *dev,
u8 *rss_config; u8 *rss_config;
u32 rss_cfg_offset = offsetof(struct ethtool_rxfh, rss_config[0]); u32 rss_cfg_offset = offsetof(struct ethtool_rxfh, rss_config[0]);
if (!(ops->get_rxfh_indir_size || ops->get_rxfh_key_size) || if (!ops->get_rxnfc || !ops->set_rxfh)
!ops->get_rxnfc || !ops->set_rxfh)
return -EOPNOTSUPP; return -EOPNOTSUPP;
if (ops->get_rxfh_indir_size) if (ops->get_rxfh_indir_size)
dev_indir_size = ops->get_rxfh_indir_size(dev); dev_indir_size = ops->get_rxfh_indir_size(dev);
if (ops->get_rxfh_key_size) if (ops->get_rxfh_key_size)
dev_key_size = dev->ethtool_ops->get_rxfh_key_size(dev); dev_key_size = dev->ethtool_ops->get_rxfh_key_size(dev);
if ((dev_key_size + dev_indir_size) == 0)
return -EOPNOTSUPP;
if (copy_from_user(&rxfh, useraddr, sizeof(rxfh))) if (copy_from_user(&rxfh, useraddr, sizeof(rxfh)))
return -EFAULT; return -EFAULT;
/* Check that reserved fields are 0 for now */ /* Check that reserved fields are 0 for now */
if (rxfh.rss_context || rxfh.rsvd[0] || rxfh.rsvd[1]) if (rxfh.rss_context || rxfh.rsvd8[0] || rxfh.rsvd8[1] ||
rxfh.rsvd8[2] || rxfh.rsvd32)
return -EINVAL; return -EINVAL;
/* If either indir or hash key is valid, proceed further. /* If either indir, hash key or function is valid, proceed further.
* It is not valid to request that both be unchanged. * Must request at least one change: indir size, hash key or function.
*/ */
if ((rxfh.indir_size && if ((rxfh.indir_size &&
rxfh.indir_size != ETH_RXFH_INDIR_NO_CHANGE && rxfh.indir_size != ETH_RXFH_INDIR_NO_CHANGE &&
rxfh.indir_size != dev_indir_size) || rxfh.indir_size != dev_indir_size) ||
(rxfh.key_size && (rxfh.key_size != dev_key_size)) || (rxfh.key_size && (rxfh.key_size != dev_key_size)) ||
(rxfh.indir_size == ETH_RXFH_INDIR_NO_CHANGE && (rxfh.indir_size == ETH_RXFH_INDIR_NO_CHANGE &&
rxfh.key_size == 0)) rxfh.key_size == 0 && rxfh.hfunc == ETH_RSS_HASH_NO_CHANGE))
return -EINVAL; return -EINVAL;
if (rxfh.indir_size != ETH_RXFH_INDIR_NO_CHANGE) if (rxfh.indir_size != ETH_RXFH_INDIR_NO_CHANGE)
...@@ -845,7 +850,7 @@ static noinline_for_stack int ethtool_set_rxfh(struct net_device *dev, ...@@ -845,7 +850,7 @@ static noinline_for_stack int ethtool_set_rxfh(struct net_device *dev,
} }
} }
ret = ops->set_rxfh(dev, indir, hkey); ret = ops->set_rxfh(dev, indir, hkey, rxfh.hfunc);
out: out:
kfree(rss_config); kfree(rss_config);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment