以太网驱动的流程浅析(一)-Ifconfig主要流程
作者:heaven 发布于:2019-12-6 15:50
分类:Linux内核分析
Author:张昺华
Time:2019年3月23日星期六
大家好,我叫张昺华,中间那个字和“饼”字一个读音。
很喜欢一群人在研究技术,一起做有意思的东西,一起分享技术带给我们的快乐,也希望中国有更多的人热爱技术,喜欢一起研究、分享技术,然后可以一起用我们的技术来做一些好玩的东西,可以为这个社会创造一些东西来改善人们的生活。
如下是本人调试过程中的一点经验分享,以太网驱动架构毕竟涉及的东西太多,如下仅仅是针对加载流程和围绕这个问题产生的分析过程和驱动加载流程部分,并不涉及以太网协议层的数据流程分析。
【硬件环境】 Imx6ul
【Linux kernel版本】 Linux4.1.15
【以太网phy】 Realtek8201f
1. 问题描述
【问题】
机器通过usb方式下载了mac地址后,发现以太网无法正常使用,敲命令ifconfig eth0 up出现:ifconfig: SIOCSIFFLAGS: No such
device,而对于没有下载以太网mac
address的机器表现均正常。调试过程中发现在以太网控制器代码中加入一些printk,不正常的机器又正常了,打印的位置不同,机器的以太网有时会正常,有时会异常,十分诡异。
2. 原因分析
【根本原因】
reset时序问题导致,phy reset的时间不满足时序要求。如下图,如果硬件接了reset引脚,应满足时序要求在reset保持10ms有效电平后,还必须维持至少150ms才可以访问phy register,也就是reset要在B点之后才可以正常通过MDC/MDIO来访问phy register。如果是不使用硬件reset,使用软件reset方式,那也要至少在A点,也就是在reset维持10ms有效电平后,再维持3.5个clk才能正常访问phy register。
那为什么下载了mac地址后才异常呢?不下载的又正常呢?
【原因分析】
freescale控制器获取mac address流程如下:
1)模块化参数设置,如果没有跳到步骤2;
2)device tree中设置,如果没有跳到步骤3;
3)from flash / fuse / via platform data,如果没有跳到步骤4;
4)FEC mac registers set by bootloader===》即靠usb方式下载mac address ,如果没有跳到步骤5;
5)靠kernel算一个随机数mac address出来,然后写入mac
那为什么下载了mac地址后才异常呢?
下了mac后,会执行步骤4,不会执行步骤5,此时目前的代码不满足150ms的时序要求,无法访问phy register,
导致phy_id获取不到,因此phy_device也不会创建
那为什么不下载的又正常呢?
不下载mac address,会执行步骤5 ,步骤5中调用了函数eth_hw_addr_random
刚好满足了150ms的时序要求,所以才可以正常
/*
* 4) FEC mac registers set by bootloader
*/
if (!is_valid_ether_addr(iap)) {
*((__be32 *) &tmpaddr[0]) =
cpu_to_be32(readl(fep->hwp + FEC_ADDR_LOW));
*((__be16 *) &tmpaddr[4]) =
cpu_to_be16(readl(fep->hwp + FEC_ADDR_HIGH) >> 16);
iap = &tmpaddr[0];
}
/*
* 5) random mac address
*/
if (!is_valid_ether_addr(iap)) {
/* Report it and use a random ethernet address instead */
netdev_err(ndev, "Invalid MAC address: %pM\n", iap);
eth_hw_addr_random(ndev);
netdev_info(ndev, "Using random MAC address: %pM\n",
ndev->dev_addr);
return;
}
跟入代码eth_hw_addr_random看下
/**
* eth_hw_addr_random - Generate software assigned random Ethernet and
* set device flag
* @dev: pointer to net_device structure
*
* Generate a random Ethernet address (MAC) to be used by a net device
* and set addr_assign_type so the state can be read by sysfs and be
* used by userspace.
*/
static inline void eth_hw_addr_random(struct net_device *dev)
{
dev->addr_assign_type = NET_ADDR_RANDOM;
eth_random_addr(dev->dev_addr);
}
继续看:
/**
* eth_random_addr - Generate software assigned random Ethernet address
* @addr: Pointer to a six-byte array containing the Ethernet address
*
* Generate a random Ethernet address (MAC) that is not multicast
* and has the local assigned bit set.
*/
static inline void eth_random_addr(u8 *addr)
{
get_random_bytes(addr, ETH_ALEN);
addr[0] &= 0xfe;/* clear multicast bit */
addr[0] |= 0x02;/* set local assignment bit (IEEE802) */
}
最终调用了kernel提供的获取随机数的一个函数,这块代码比较多就不继续追下去了。
/*
* This function is the exported kernel interface. It returns some
* number of good random numbers, suitable for key generation, seeding
* TCP sequence numbers, etc. It does not rely on the hardware random
* number generator. For random bytes direct from the hardware RNG
* (when available), use get_random_bytes_arch().
*/
void get_random_bytes(void *buf, int nbytes)
{
#if DEBUG_RANDOM_BOOT > 0
if (unlikely(nonblocking_pool.initialized == 0))
printk(KERN_NOTICE "random: %pF get_random_bytes called "
"with %d bits of entropy available\n",
(void *) _RET_IP_,
nonblocking_pool.entropy_total);
#endif
trace_get_random_bytes(nbytes, _RET_IP_);
extract_entropy(&nonblocking_pool, buf, nbytes, 0, 0);
}
EXPORT_SYMBOL(get_random_bytes);
所以这块步骤五的代码刚刚好好在这个硬件条件下,恰巧满足了150ms的reset时序要求,所以以太网才可以正常。
3. 以太网流程分析跟踪
3.1 Ifconfig主要流程
回归主题,根据这个ifconfig失败的现象,我们追踪一下code:
ifconfig: SIOCSIFFLAGS: No such device,既然出现了这个问题log,我们就从应用层的log入手,首先我们使用strace命令来追踪下系统调用,以便于我们追踪内核代码实现。
strace ifconfig eth0 up跟踪一下
可以发现主要是ioctl的操作,SIOCSIFFLAGS,然后我们需要了解下这个宏的意思,说白了就是设置各种flag,靠ioctl第三个参数把所需要的动作flag传入,比如说此时要对eth0进行up动作,那么就传入IFF_UP,例如:
struct ifreq ifr;
我们看这些主要是想知道为什么会打印这个log:
ifconfig: SIOCSIFFLAGS:
No such device
那么内核中又是对ioctl做了什么动作呢?因为strace命令让我们知道了系统调用调用函数,我们可以在kernel中直接搜索SIOCSIFFLAGS,或者去以太网驱动net目录下直接搜索更快。最终我搜到了,路径是:net/ipv4/devinet.c
我们可以看到内核的宏定义:
/* Socket configuration controls. */
#define SIOCGIFNAME0x8910/* get iface name*/
#define SIOCSIFLINK0x8911/* set iface channel*/
#define SIOCGIFCONF0x8912/* get iface list*/
#define SIOCGIFFLAGS0x8913/* get flags*/
#define SIOCSIFFLAGS0x8914/* set flags*/
#define SIOCGIFADDR0x8915/* get PA address*/
#define SIOCSIFADDR0x8916/* set PA address*/
#define SIOCGIFDSTADDR0x8917/* get remote PA address*/
#define SIOCSIFDSTADDR0x8918/* set remote PA address*/
#define SIOCGIFBRDADDR0x8919/* get broadcast PA address*/
#define SIOCSIFBRDADDR0x891a/* set broadcast PA address*/
#define SIOCGIFNETMASK0x891b/* get network PA mask*/
#define SIOCSIFNETMASK0x891c/* set network PA mask*/
#define SIOCGIFMETRIC0x891d/* get metric*/
#define SIOCSIFMETRIC0x891e/* set metric*/
#define SIOCGIFMEM0x891f/* get memory address (BSD)*/
#define SIOCSIFMEM0x8920/* set memory address (BSD)*/
#define SIOCGIFMTU0x8921/* get MTU size*/
#define SIOCSIFMTU0x8922/* set MTU size*/
#define SIOCSIFNAME0x8923/* set interface name */
#defineSIOCSIFHWADDR0x8924/* set hardware address */
#define SIOCGIFENCAP0x8925/* get/set encapsulations */
#define SIOCSIFENCAP0x8926
#define SIOCGIFHWADDR0x8927/* Get hardware address*/
#define SIOCGIFSLAVE0x8929/* Driver slaving support*/
#define SIOCSIFSLAVE0x8930
#define SIOCADDMULTI0x8931/* Multicast address lists*/
#define SIOCDELMULTI0x8932
#define SIOCGIFINDEX0x8933/* name -> if_index mapping*/
#define SIOGIFINDEXSIOCGIFINDEX/* misprint compatibility :-)*/
#define SIOCSIFPFLAGS0x8934/* set/get extended flags set*/
#define SIOCGIFPFLAGS0x8935
#define SIOCDIFADDR0x8936/* delete PA address*/
#defineSIOCSIFHWBROADCAST0x8937/* set hardware broadcast addr*/
#define SIOCGIFCOUNT0x8938/* get number of devices */
#define SIOCGIFBR0x8940/* Bridging support*/
#define SIOCSIFBR0x8941/* Set bridging options */
#define SIOCGIFTXQLEN0x8942/* Get the tx queue length*/
#define SIOCSIFTXQLEN0x8943/* Set the tx queue length */
查看devinet.c的代码,我们找到了那个宏,也就是做devinet_ioctl函数中,这也就是应用层的ioctl最终的实现函数,然后我们在里面加一些打印,
通过打印结果我们可以确认是这个函数devinet_ioctl为应用层的ioctl的实现函数,因为你在kernel中搜SIOCSIFFLAGS宏的话会有很多地方出现的,所以我们需要确认我们找的函数没问题:
看到这里返回值ret是-19,那么我们继续顺着追踪下去,上代码:
net/core/dev.c
/**
*dev_change_flags - change device settings
*@dev: device
*@flags: device state flags
*
*Change settings on device based state flags. The flags are
*in the userspace exported format.
*/
int dev_change_flags(struct net_device *dev, unsigned int flags)
{
int ret;
unsigned int changes, old_flags = dev->flags, old_gflags = dev->gflags;
ret = __dev_change_flags(dev, flags);
if (ret < 0)
return ret;
changes = (old_flags ^ dev->flags) | (old_gflags ^ dev->gflags);
__dev_notify_flags(dev, old_flags, changes);
return ret;
}
EXPORT_SYMBOL(dev_change_flags);
继续追踪:net/core/dev.c
int __dev_change_flags(struct net_device *dev, unsigned int flags)
{
unsigned int old_flags = dev->flags;
int ret;
ASSERT_RTNL();
/*
*Set the flags on our device.
*/
dev->flags = (flags & (IFF_DEBUG | IFF_NOTRAILERS | IFF_NOARP |
IFF_DYNAMIC | IFF_MULTICAST | IFF_PORTSEL |
IFF_AUTOMEDIA)) |
(dev->flags & (IFF_UP | IFF_VOLATILE | IFF_PROMISC |
IFF_ALLMULTI));
/*
*Load in the correct multicast list now the flags have changed.
*/
if ((old_flags ^ flags) & IFF_MULTICAST)
dev_change_rx_flags(dev, IFF_MULTICAST);
dev_set_rx_mode(dev);
/*
*Have we downed the interface. We handle IFF_UP ourselves
*according to user attempts to set it, rather than blindly
*setting it.
*/
ret = 0;
if ((old_flags ^ flags) & IFF_UP)
ret = ((old_flags & IFF_UP) ? __dev_close : __dev_open)(dev);
if ((flags ^ dev->gflags) & IFF_PROMISC) {
int inc = (flags & IFF_PROMISC) ? 1 : -1;
unsigned int old_flags = dev->flags;
dev->gflags ^= IFF_PROMISC;
if (__dev_set_promiscuity(dev, inc, false) >= 0)
if (dev->flags != old_flags)
dev_set_rx_mode(dev);
}
/* NOTE: order of synchronization of IFF_PROMISC and IFF_ALLMULTI
is important. Some (broken) drivers set IFF_PROMISC, when
IFF_ALLMULTI is requested not asking us and not reporting.
*/
if ((flags ^ dev->gflags) & IFF_ALLMULTI) {
int inc = (flags & IFF_ALLMULTI) ? 1 : -1;
dev->gflags ^= IFF_ALLMULTI;
__dev_set_allmulti(dev, inc, false);
}
return ret;
}
因此我们可以看到返回值-19就是如下代码产生的
/*
*Have we downed the interface. We handle IFF_UP ourselves
*according to user attempts to set it, rather than blindly
*setting it.
*/
ret = 0;
if ((old_flags ^ flags) & IFF_UP)
ret = ((old_flags & IFF_UP) ? __dev_close : __dev_open)(dev);
因此我们需要追踪__dev_open函数,继续看代码:
static int __dev_open(struct net_device *dev)
{
const struct net_device_ops *ops = dev->netdev_ops;
int ret;
ASSERT_RTNL();
if (!netif_device_present(dev))
return -ENODEV;
/* Block netpoll from trying to do any rx path servicing.
* If we don't do this there is a chance ndo_poll_controller
* or ndo_poll may be running while we open the device
*/
netpoll_poll_disable(dev);
ret = call_netdevice_notifiers(NETDEV_PRE_UP, dev);
ret = notifier_to_errno(ret);
if (ret)
return ret;
set_bit(__LINK_STATE_START, &dev->state);
if (ops->ndo_validate_addr)
ret = ops->ndo_validate_addr(dev);
if (!ret && ops->ndo_open)
ret = ops->ndo_open(dev);
netpoll_poll_enable(dev);
if (ret)
clear_bit(__LINK_STATE_START, &dev->state);
else {
dev->flags |= IFF_UP;
dev_set_rx_mode(dev);
dev_activate(dev);
add_device_randomness(dev->dev_addr, dev->addr_len);
}
return ret;
}
通过调试,比如说加打印,或者是经验我们可以推断出是这里返回的-19,那么这个ndo_open又是在哪里回调的呢?
if (!ret && ops->ndo_open)
ret = ops->ndo_open(dev);
我们可以看到ops这个结构的结构体
struct net_device *dev
const struct net_device_ops *ops =
dev->netdev_ops;
/*
* This structure defines the management hooks for network devices.
* The following hooks can be defined; unless noted otherwise, they are
* optional and can be filled with a null pointer.
*
* int (*ndo_init)(struct net_device *dev);
* This function is called once when network device is registered.
* The network device can use this to any late stage initializaton
* or semantic validattion. It can fail with an error code which will
* be propogated back to register_netdev
*
* void (*ndo_uninit)(struct net_device *dev);
* This function is called when device is unregistered or when registration
* fails. It is not called if init fails.
*
* int (*ndo_open)(struct net_device *dev);
* This function is called when network device transistions to the up
* state.
*
* int (*ndo_stop)(struct net_device *dev);
* This function is called when network device transistions to the down
* state.
struct net_device_ops {
int(*ndo_init)(struct net_device *dev);
void(*ndo_uninit)(struct net_device *dev);
int(*ndo_open)(struct net_device *dev);
int(*ndo_stop)(struct net_device *dev);
netdev_tx_t(*ndo_start_xmit) (struct sk_buff *skb,
struct net_device *dev);
u16(*ndo_select_queue)(struct net_device *dev,
这里熟悉驱动的朋友应该可以猜到这在在freescale的以太网控制器驱动中一定有它的实现
net_device_ops就是kernel提供给drvier操作net_device的一些操作方法,具体实现自然由相应厂商的driver自己去实现。
路径:drivers/net/Ethernet/freescale/fec_main.c
static const struct net_device_ops fec_netdev_ops = {
.ndo_open= fec_enet_open,
.ndo_stop= fec_enet_close,
.ndo_start_xmit= fec_enet_start_xmit,
.ndo_select_queue = fec_enet_select_queue,
.ndo_set_rx_mode= set_multicast_list,
.ndo_change_mtu= eth_change_mtu,
.ndo_validate_addr= eth_validate_addr,
.ndo_tx_timeout= fec_timeout,
.ndo_set_mac_address= fec_set_mac_address,
.ndo_do_ioctl= fec_enet_ioctl,
#ifdef CONFIG_NET_POLL_CONTROLLER
.ndo_poll_controller= fec_poll_controller,
#endif
.ndo_set_features= fec_set_features,
};
我们可以在这个fec_enet_open函数中加入dump_stack来看下整个调用情况
我们打出kernel的dump_stack信息来看:
这个调用过程就是应用层ioctl一直到kernel最底层fec_enet_open的过程。
应用代码这样:
总体流程:kill()
-> kill.S -> swi陷入内核态 -> 从sys_call_table查看到sys_kill -> ret_fast_syscall -> 回到用户态执行kill()下一行代码
Ioctl《==ret_fast_syscall《==SyS_ioctl《==do_vfs_ioctl《==vfs_ioctl《==sock_ioctl《==
devinet_ioctl《==dev_change_flags《==__dev_change_flags《==__dev_open《==fec_enet_open
我附上每个函数的代码:
如果大家想看系统调用流程的话,参考这篇,我就不做这块的说明了:
Linux系统调用(syscall)原理
4. 网址分享
linux PHY驱动
Linux PHY几个状态的跟踪
第十六章PHY -基于Linux3.10
评论:
gavin
2020-03-07 10:48
这个问题原因很难找啊,楼主是怎么想到时间问题的啊,最终是通过延时时间来解决吗?
2020-03-22 21:23
@gavin:解决方案就是按照芯片时序,满足reset的时间要求,也就是需延时处理即可。
解决思路我有写下来,1~5就是我分析的过程,我一开始也是不知道为什么是这里,因为是第一次做以太网这块的问题,所以就趁此机会把ifconfig的整个流程通读了一遍,清楚了来龙去脉其实就比较好分析了
juno
2021-01-18 14:03
@gavin:我在写以太网驱动的时候,在reset的时候都会加载延迟,比如用sleep_range这样的函来等待一段时间再执行后边的流程,这样可以防止有些phy启动慢的问题。
这个等待不只是以太网需要注意,pcie驱动也要注意,pcie host在启动的时候,也会发reset信号, 如果硬件有接reset,也会硬件复位外设,如果reset之后立马读config寄存器,也会有概率读不到设备,导致pcie设备无法正常识别。(pcie需要host先起来,然后探测到设备,加入到pcie总线,开启相应的时钟,如果没有探测到设备,就会关闭时钟,pcie总线上也不会有设备,后期pcie device驱动加载的时候就发现不了设备)
marsqian
2019-12-31 10:56
请问下配置MAC地址直接用ifconfig eth0 hw ether xx:xx:xx:xx:xx:xx命令可以的吗?还是说Imx6ul必须要usb方式下载了mac呢
2020-01-06 13:59
@marsqian:对于imx6ul来讲,ifconfig的方式只适用于不掉电的情况,重启后,mac地址会恢复成默认的,如果想永久改写,必须使用他们的工具选择usb下载方式
2020-01-06 14:02
@marsqian:刚表述有些不精准,准确的说不是恢复成默认的,而是看你们的driver code的那几个步骤:
1)模块化参数设置,如果没有跳到步骤2;
2)device tree中设置,如果没有跳到步骤3;
3)from flash / fuse / via platform data,如果没有跳到步骤4;
4)FEC mac registers set by bootloader===》即靠usb方式下载mac address ,如果没有跳到步骤5;
5)靠kernel算一个随机数mac address出来,然后写入mac
所以你们可以选择123步骤中设置好mac地址即可,就可以永久改写mac地址,就不需要usb的方式了,或者你们非想要实现ifconfig的方式来设置,可以修改你们自身的driver来实现这个功能
Archer
2019-12-27 14:19
后面的代码分析很NICE。 也可以在uboot层做自定义的reset handle一劳永逸
2019-12-27 16:16
@Archer:嗯嗯,你说的也是一种办法,这就看哪段做适配了,不同的phy很可能时序不一样,个人感觉让uboot去做mac或者phy的适配的代码感觉有点冗余,drvier层面做不同的phy的时序控制会更灵活一些,drvier提供时序的控制方法,dts去根据不同的板子情况来做不同的时序调整,一个os,一个drvier即可,这是我个人的想法
2019-12-25 11:56
要写文章摘要。
2019-12-27 17:23
@沙漠之狐:原来写了文章摘要才会出现阅读全文的这个按钮,感谢提醒
carolyn
2019-12-25 09:35
赞
发表评论:
昵称
邮件地址 (选填)
个人主页 (选填)