# xv6 network-net
快速記錄一下在xv6 system 上如果要實現一個網卡驅動大致上會經過哪一些環節,算是興趣的很想知道一個OS如何實現網卡連接外部設備我們直接跳最後一個lab來了解一下,這邊只是粗略分析,結合以前看的一些東西做一些聯想與總結,在某些部分可能有理解錯誤請見諒。
# main.c
從 asm 一路跳轉過來 main ,讓作業系統可以透過 c 語言去撰寫,中間會經過很多設置,這邊我們專注在code 整體流程來trace 一下
```c=
#ifdef LAB_NET
pci_init();
sockinit();
```
![](https://i.imgur.com/vLAd3g7.png)
# pci_init
可以看到 pci 透過 qemu 可能會在記憶體初始化的時候加載於一個記憶體區段,於vm.c 可以進一步得知register 會放在這裡,下面就是在尋找有關於網卡的id
> // 100e:8086 is an e1000
> if(id == 0x100e8086){
```c=
void
pci_init()
{
printf("e1000 enable \n" );
// we'll place the e1000 registers at this address.
// vm.c maps this range.
uint64 e1000_regs = 0x40000000L;
// qemu -machine virt puts PCIe config space here.
// vm.c maps this range.
uint32 *ecam = (uint32 *) 0x30000000L;
// look at each possible PCI device on bus 0.
for(int dev = 0; dev < 32; dev++){
int bus = 0;
int func = 0;
int offset = 0;
uint32 off = (bus << 16) | (dev << 11) | (func << 8) | (offset);
volatile uint32 *base = ecam + off;
uint32 id = base[0];
// 100e:8086 is an e1000
if(id == 0x100e8086){
```
# vm.c
![](https://i.imgur.com/jTmRRqL.png)
```c=
#ifdef LAB_NET
// PCI-E ECAM (configuration space), for pci.c
kvmmap(kpgtbl, 0x30000000L, 0x30000000L, 0x10000000, PTE_R | PTE_W);
// pci.c maps the e1000's registers here.
kvmmap(kpgtbl, 0x40000000L, 0x40000000L, 0x20000, PTE_R | PTE_W);
```
初步推理可以得到在中斷pic 找到網卡後會進行 初始化的動作
![](https://i.imgur.com/Wvh6JOi.png)
```c=
e1000_init((uint32*)e1000_regs);
```
# e1000.c
到這邊可以看到實際上的網卡,可以想成現在網卡的register 都會寫到我們剛剛的記憶體位置
uint64 e1000_regs = 0x40000000L;
![](https://i.imgur.com/16upOV9.png)
可以關注於
tx_ring
rx_ring
在一般中斷可能會透過 pic 主動對 system 發生interrupt ,但是面對 滑鼠 網卡這些總不可能持續的對作業系統發生中斷,這會讓cpu 無法為其他process 進行系統調度,所以可能視情況
polling ,或者讓 device 自行發出中斷,
另一方面的設計概念在30 day os 也有 也就是環狀的 buffer主要就是當你的設備遇到組合鍵,或者,連續得input 你的array 總不可能大小是無限大,那麼解決這種問題環狀的buffer 就是比較好的解法,當然用在教學用的xv6 作業系統也是比較好實作。
```c=
// [E1000 14.5] Transmit initialization
memset(tx_ring, 0, sizeof(tx_ring));
for (i = 0; i < TX_RING_SIZE; i++) {
tx_ring[i].status = E1000_TXD_STAT_DD;
tx_mbufs[i] = 0;
}
regs[E1000_TDBAL] = (uint64) tx_ring;
if(sizeof(tx_ring) % 128 != 0)
panic("e1000");
regs[E1000_TDLEN] = sizeof(tx_ring);
regs[E1000_TDH] = regs[E1000_TDT] = 0;
// [E1000 14.4] Receive initialization
memset(rx_ring, 0, sizeof(rx_ring));
for (i = 0; i < RX_RING_SIZE; i++) {
rx_mbufs[i] = mbufalloc(0);
if (!rx_mbufs[i])
panic("e1000");
rx_ring[i].addr = (uint64) rx_mbufs[i]->head;
}
regs[E1000_RDBAL] = (uint64) rx_ring;
if(sizeof(rx_ring) % 128 != 0)
panic("e1000");
regs[E1000_RDH] = 0;
regs[E1000_RDT] = RX_RING_SIZE - 1;
regs[E1000_RDLEN] = sizeof(rx_ring);
```
# mac setting
// filter by qemu's MAC address, 52:54:00:12:34:56
當網卡在一般區域網路可能可以用 MAC 當作唯一值,但是到互聯網就會需要結合 IP 來產生一組唯一值,透過ROUTER 層解析 IP HEADER 進一步把資料送到你家
```c=
// filter by qemu's MAC address, 52:54:00:12:34:56
regs[E1000_RA] = 0x12005452;
regs[E1000_RA+1] = 0x5634 | (1<<31);
```
後面就是一些 REGISTER 的操作,讓網卡可以根據BIT 來決定動作
```c=
// transmitter control bits.
regs[E1000_TCTL] = E1000_TCTL_EN | // enable
E1000_TCTL_PSP | // pad short packets
(0x10 << E1000_TCTL_CT_SHIFT) | // collision stuff
(0x40 << E1000_TCTL_COLD_SHIFT);
regs[E1000_TIPG] = 10 | (8<<10) | (6<<20); // inter-pkt gap
// receiver control bits.
regs[E1000_RCTL] = E1000_RCTL_EN | // enable receiver
E1000_RCTL_BAM | // enable broadcast
E1000_RCTL_SZ_2048 | // 2048-byte rx buffers
E1000_RCTL_SECRC; // strip CRC
// ask e1000 for receive interrupts.
regs[E1000_RDTR] = 0; // interrupt after every received packet (no timer)
regs[E1000_RADV] = 0; // interrupt after every packet (no timer)
regs[E1000_IMS] = (1 << 7); // RXDW -- Receiver Descriptor Write Back
```
# TX & RX
在實驗步驟可能要完成這兩個FUCNTION 才能進一步的通過 unittest,在這兩個裡面到底會經過哪一些步驟我們慢慢看,可以觀察裡面的tx_ring 和 buffer 到底是什麼,我們回到 xv6 的 file_system 實現
![](https://i.imgur.com/f900GjL.png)
```c=
#define TX_RING_SIZE 16
static struct tx_desc tx_ring[TX_RING_SIZE] __attribute__((aligned(16)));
static struct mbuf *tx_mbufs[TX_RING_SIZE];
#define RX_RING_SIZE 16
static struct rx_desc rx_ring[RX_RING_SIZE] __attribute__((aligned(16)));
static struct mbuf *rx_mbufs[RX_RING_SIZE];
```
```c=
if (tx_ring[tail].status != E1000_TXD_STAT_DD) {
```
```c=
if(tx_mbufs[tail])
```
```c=
while (rx_ring[i].status & E1000_RXD_STAT_DD) {
```
```c=
net_rx(rx_mbufs[i]);
```
## int e1000_transmit(struct mbuf* m)
```c=
int
e1000_transmit(struct mbuf* m)
{
//
// Your code here.
//
// the mbuf contains an ethernet frame; program it into
// the TX descriptor ring so that the e1000 sends it. Stash
// a pointer so that it can be freed after sending.
//
acquire(&e1000_lock);
uint32 tail = regs[E1000_TDT];
// overflow
if (tx_ring[tail].status != E1000_TXD_STAT_DD) {
release(&e1000_lock);
return -1;
}
if(tx_mbufs[tail]){
mbuffree(tx_mbufs[tail]);
}
tx_ring[tail].length = (uint16)m->len;
tx_ring[tail].addr = (uint64)m->head;
tx_ring[tail].cmd = 9;
tx_mbufs[tail] = m;
regs[E1000_TDT] = (tail+1)%TX_RING_SIZE;
release(&e1000_lock);
return 0;
}
```
## static void e1000_recv(void)
```c=
static void
e1000_recv(void)
{
//
// Your code here.
//
// Check for packets that have arrived from the e1000
// Create and deliver an mbuf for each packet (using net_rx()).
//
int tail = regs[E1000_RDT];
int i = (tail+1)%RX_RING_SIZE; // tail is owned by Hardware!
while (rx_ring[i].status & E1000_RXD_STAT_DD) {
rx_mbufs[i]->len = rx_ring[i].length;
// send mbuf to upper level (the network stack in net.c).
net_rx(rx_mbufs[i]);
// get a new buffer for next recv.
rx_mbufs[i] = mbufalloc(0);
rx_ring[i].addr = (uint64)rx_mbufs[i]->head;
// update status for next recv.
rx_ring[i].status = 0;
i = (i + 1) % RX_RING_SIZE;
}
regs[E1000_RDT] = i - 1; // - 1 for the while loop.
}
```
# xv6 filesystem / network
我們知道linux 的設計概念為,一切皆是文件
在剛才的部分不管是
mbuf 還是 TX_RING,RX_RING 看起來就像是把資料放在 BUFFER ,那麼 buffer 的元素放置的其實放的就是 檔案
端看結構體可以稍微猜一下剛才我們的
環狀buffer 長這樣
https://zhuanlan.zhihu.com/p/351563871
![](https://i.imgur.com/S6Bvf5z.png)
透過讀寫 register 的狀態,因為在udp/tcp 可能在某一些情況光靠軟體的 lock 可能不太可靠,更多可能要依賴 硬體的 register 狀態 或者 flag 來進行精準判斷 資料是否已經傳輸完畢,或者描述各種狀態
那麼當資料進來的時候又會發生什麼事情。
# lwip
這邊只是稍微的看過可以把他想成,在面對無 os 或者 有 os 又有網卡的 嵌入式系統怎麼進行 網卡上的傳輸,所以就有人利用軟體在硬體上實現一個抽象層,讓嵌入式可以進行資料上的傳輸
![](https://i.imgur.com/mQ0fQzZ.png)
透過封裝過後的 system_call 可以更好的銜接到我們的 os 以便可以實現網路卡驅動的部分。
我這邊把網路的data 想成不斷的在資料的片段上去加上 ip header ,這邊又會涉及到 socket 的原理機制我們從測資上 回推 整個設計概念
![](https://i.imgur.com/BSNMlAx.png)
/// ping(2000, dport, 1);
# nettest / ping
```c=
ping(2000, dport, 1);
```
![](https://i.imgur.com/wZ8fDSA.png)
撇除一切比較關於傳輸協議的東西,我們回歸上一個小章節,linux 設計概念一切皆為檔案的方式,可以看到 下列udp 傳輸範例裡面又是對 file 去做操作,當然在實驗裡面沒有實現server 的功能,只有簡單的 udp 去建立的傳輸,下列是使用udp 協議通訊的程式碼
```c=
if ((fd = connect(dst, sport, dport)) < 0)
{
fprintf(2, "ping: connect() failed\n");
exit(1);
}
for(int i = 0; i < attempts; i++) {
if(write(fd, obuf, strlen(obuf)) < 0){
fprintf(2, "ping: send() failed\n");
exit(1);
}
}
char ibuf[128];
int cc = read(fd, ibuf, sizeof(ibuf) - 1);
```
我們往system call 去查看
# user.c
在更前面的例子有敘述怎麼構造 system call 的範例
## connect
![](https://i.imgur.com/66DaCDj.png)
```c=
#ifdef LAB_NET
int
sys_connect(void)
{
struct file *f;
int fd;
uint32 raddr;
uint32 rport;
uint32 lport;
if (argint(0, (int*)&raddr) < 0 ||
argint(1, (int*)&lport) < 0 ||
argint(2, (int*)&rport) < 0) {
return -1;
}
if(sockalloc(&f, raddr, lport, rport) < 0)
return -1;
if((fd=fdalloc(f)) < 0){
fileclose(f);
return -1;
}
return fd;
}
#endif
```
專注於 if(sockalloc(&f, raddr, lport, rport) < 0)
可以看到當我們去 connect 其實就是去產生一個檔案,其中也可以看到檔案的type 設為
(*f)->type = FD_SOCK;
```c=
int
sockalloc(struct file **f, uint32 raddr, uint16 lport, uint16 rport)
{
struct sock *si, *pos;
si = 0;
*f = 0;
if ((*f = filealloc()) == 0)
goto bad;
if ((si = (struct sock*)kalloc()) == 0)
goto bad;
// initialize objects
si->raddr = raddr;
si->lport = lport;
si->rport = rport;
initlock(&si->lock, "sock");
mbufq_init(&si->rxq);
(*f)->type = FD_SOCK;
(*f)->readable = 1;
(*f)->writable = 1;
(*f)->sock = si;
// add to list of sockets
acquire(&lock);
pos = sockets;
while (pos) {
if (pos->raddr == raddr &&
pos->lport == lport &&
pos->rport == rport) {
release(&lock);
goto bad;
}
pos = pos->next;
}
si->next = sockets;
sockets = si;
release(&lock);
return 0;
bad:
if (si)
kfree((char*)si);
if (*f)
fileclose(*f);
return -1;
}
```
我們可以建立很多 socket,這邊我的理解是在網路你可能會有很多個連線連進來,一個 socket 是絕對不夠用的所以有個 list 來進行管理
![](https://i.imgur.com/IaBEY3X.png)
```c=
struct sock {
struct sock *next; // the next socket in the list
uint32 raddr; // the remote IPv4 address
uint16 lport; // the local UDP port number
uint16 rport; // the remote UDP port number
struct spinlock lock; // protects the rxq
struct mbufq rxq; // a queue of packets waiting to be received
};
```
如果已經找到已有的ip 和 port
則不能連線
```c=
if (pos->raddr == raddr &&
pos->lport == lport &&
pos->rport == rport) {
release(&lock);
goto bad;
```
```c=
bad:
if (si)
kfree((char*)si);
if (*f)
fileclose(*f);
return -1;
```
## file write/read
仔細看system call 可以看到他的設計概念連到
fileread ,filewrite
其實後面底層決定讀寫要怎麼處理在前面我們有提過 FD_SOCK我們統一對 fd 去做讀寫將會判斷這個flag 來進行socket 的調用
```c=
uint64
sys_read(void)
{
struct file *f;
int n;
uint64 p;
if(argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argaddr(1, &p) < 0)
return -1;
return fileread(f, p, n);
}
uint64
sys_write(void)
{
struct file *f;
int n;
uint64 p;
if(argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argaddr(1, &p) < 0)
return -1;
return filewrite(f, p, n);
}
```
### filewrite
```c=
#ifdef LAB_NET
else if(f->type == FD_SOCK){
r = sockread(f->sock, addr, n);
}
```
### fileread
```c=
#ifdef LAB_NET
else if(f->type == FD_SOCK){
ret = sockwrite(f->sock, addr, n);
}
```
不管是sockread ,sockwrite 最後都會走到我們網卡最先的完成的兩個fucntion e1000_transmit ,e1000_recv
### sockread
```c=
int
sockread(struct sock *si, uint64 addr, int n)
{
struct proc *pr = myproc();
struct mbuf *m;
int len;
acquire(&si->lock);
printf("teet3\n");
while (mbufq_empty(&si->rxq) && !pr->killed) {
sleep(&si->rxq, &si->lock);
}
printf("teet\n");
if (pr->killed) {
release(&si->lock);
return -1;
}
m = mbufq_pophead(&si->rxq);
release(&si->lock);
len = m->len;
if (len > n)
len = n;
if (copyout(pr->pagetable, addr, m->head, len) == -1) {
mbuffree(m);
return -1;
}
mbuffree(m);
return len;
}
```
### sockwrite
```c=
int
sockwrite(struct sock *si, uint64 addr, int n)
{
struct proc *pr = myproc();
struct mbuf *m;
m = mbufalloc(MBUF_DEFAULT_HEADROOM);
if (!m)
return -1;
if (copyin(pr->pagetable, mbufput(m, n), addr, n) == -1) {
mbuffree(m);
return -1;
}
net_tx_udp(m, si->raddr, si->lport, si->rport);
return n;
}
```
# lwip 移植大概概念
![](https://i.imgur.com/XcvbYn0.jpg)
https://codinglover.top/2021/06/26/lwip%E5%BA%94%E7%94%A8%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9Alwip%E7%A7%BB%E6%A4%8D%E7%9A%84%E4%B8%80%E4%BA%9B%E9%A2%84%E5%A4%87%E7%9F%A5%E8%AF%86/
上面是lwip 移植到一個 os
就拿 sockwrite
來說 他會走net_tx_udp跟這張圖做對比可以把他想成不斷的對我們的data 段,進行 ip header 的封裝
### net_tx_udp
```c=
// sends a UDP packet
void
net_tx_udp(struct mbuf *m, uint32 dip,
uint16 sport, uint16 dport)
{
struct udp *udphdr;
// put the UDP header
udphdr = mbufpushhdr(m, *udphdr);
udphdr->sport = htons(sport);
udphdr->dport = htons(dport);
udphdr->ulen = htons(m->len);
udphdr->sum = 0; // zero means no checksum is provided
// now on to the IP layer
net_tx_ip(m, IPPROTO_UDP, dip);
}
```
### net_tx_ip
```c=
// sends an IP packet
static void
net_tx_ip(struct mbuf *m, uint8 proto, uint32 dip)
{
struct ip *iphdr;
// push the IP header
iphdr = mbufpushhdr(m, *iphdr);
memset(iphdr, 0, sizeof(*iphdr));
iphdr->ip_vhl = (4 << 4) | (20 >> 2);
iphdr->ip_p = proto;
iphdr->ip_src = htonl(local_ip);
iphdr->ip_dst = htonl(dip);
iphdr->ip_len = htons(m->len);
iphdr->ip_ttl = 100;
iphdr->ip_sum = in_cksum((unsigned char *)iphdr, sizeof(*iphdr));
// now on to the ethernet layer
net_tx_eth(m, ETHTYPE_IP);
}
```
### net_tx_eth
```c=
static void
net_tx_eth(struct mbuf *m, uint16 ethtype)
{
struct eth *ethhdr;
ethhdr = mbufpushhdr(m, *ethhdr);
memmove(ethhdr->shost, local_mac, ETHADDR_LEN);
// In a real networking stack, dhost would be set to the address discovered
// through ARP. Because we don't support enough of the ARP protocol, set it
// to broadcast instead.
memmove(ethhdr->dhost, broadcast_mac, ETHADDR_LEN);
ethhdr->type = htons(ethtype);
if (e1000_transmit(m)) {
mbuffree(m);
}
}
```
可以看到fucntion 最後還是透過
e1000_transmit 去送出一個packet
到此就是比較完整的一個trace ,以這個LAB 來說完成的可能頂多算點對點,要完成 SERVER 的話可能進一步的了解SOCKET
![](https://i.imgur.com/2zETIed.png)
目前在看這一塊,bind 我想大概就是去監控 sockets 看要用polling 方式去定時查recv 的 packet,當發生有資料進來的時候,在去看bind 查到有沒有對應的端口, listen 看端口有沒有在監聽,有的會就accept 到這邊的話可能要結合一些tcp 的概念,當然要實現一些自定義的通訊協定也可以,粗略的lab 6 概念大概是這樣。