1. 程式人生 > 實用技巧 >mit 6.828學習筆記不知道幾--lab6

mit 6.828學習筆記不知道幾--lab6

Part A

exercise 1:

為kern/trap.c中的每個時鐘中斷新增對time_tick的呼叫。實現sys_time_msec並將其新增到kern/syscall.c中的syscall中,以便使用者空間能夠訪問時間。
在kern/trap.c中新增分支

注意不要傻fufu的重新新增一個分支,在lab4中我們已經添加了時鐘中斷,此處只要修改一下即可

else if(tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER){
        lapic_eoi(); 
        time_tick();
        sched_yield();
        
return; }

在kern/syscall.c中給syscall新增分支:

case(SYS_time_msec):
            return sys_time_msec();

實現sys_time_msec

在此之前,首先看一下kern/time.c中的幾個函式:

static unsigned int ticks;

void
time_init(void)
{
    ticks = 0;
}

// This should be called once per timer interrupt.  A timer interrupt
// fires every 10 ms.
void time_tick(void) { ticks++; if (ticks * 10 < ticks) panic("time_tick: time overflowed"); } unsigned int time_msec(void) { return ticks * 10; }

ticks在time.c進行了定義。

所以不能在sys_time_msec中直接ticks * 10,而是要呼叫time_msec(),否則會報錯ticks未定義。

// Return the current time.
static int
sys_time_msec(
void) { // LAB 6: Your code here. return time_msec(); //panic("sys_time_msec not implemented"); }

鍵入make INIT_CFLAGS=-DTEST_NO_NS run-testtime

來測試你的程式碼. 你應當看到程序在1秒內從5開始倒數,“-DTEST_NO_NS”會禁用啟動網路伺服器程序,因為它會引起panic。

exercise 2:

閱讀文件

exercise 3:

首先看看kern/pci.c中的幾個函式:

// pci_attach_class matches the class and subclass of a PCI device
struct pci_driver pci_attach_class[] = {
    { PCI_CLASS_BRIDGE, PCI_SUBCLASS_BRIDGE_PCI, &pci_bridge_attach },
    { 0, 0, 0 },
};

// pci_attach_vendor matches the vendor ID and device ID of a PCI device. key1
// and key2 should be the vendor ID and device ID respectively
//第一引數應該是venderID,第二個引數是deviseID
struct pci_driver pci_attach_vendor[] = { { PCI_E1000_VENDOR, PCI_E1000_DEVICE, &pci_e1000_attach }, { 0, 0, 0 }, };

pci_attach_class和pci_attach_vendor2個數組就是裝置陣列

對於pci裝置而言

PCI是外圍裝置互連(Peripheral Component Interconnect)的簡稱,是在目前計算機系統中得到廣泛應用的通用匯流排介面標準:

--- 在一個PCI系統中,最多可以有256根PCI匯流排,一般主機上只會用到其中很少的幾條。
--- 在一根PCI總線上可以連線多個物理裝置,可以是一個網絡卡、顯示卡或者音效卡等,最多不超過32個。
--- 一個PCI物理裝置可以有多個功能,比如同時提供視訊解析和聲音解析,最多可提供8個功能。
--- 每個功能對應1個256位元組的PCI配置空間。
//負責設定需要讀寫的具體裝置
static
void pci_conf1_set_addr(uint32_t bus, uint32_t dev, uint32_t func, uint32_t offset) { assert(bus < 256); //8位 最多可以有256根PCI匯流排,一般主機上只會用到其中很少的幾根 assert(dev < 32); //5位 一根PCI匯流排可以連線多個物理裝置,可以是一個網絡卡、顯示卡或音效卡等,最多不超過32個 assert(func < 8); //3位 一個PCI物理裝置可以有多個功能,比如同時提供視訊解析和聲音解析,最多可提供8個功能。 assert(offset < 256); //8位 每個功能對應1個256位元組的PCI配置空間。 assert((offset & 0x3) == 0);//最後兩位必須為00? uint32_t v = (1 << 31) | // config-space (bus << 16) | (dev << 11) | (func << 8) | (offset); outl(pci_conf1_addr_ioport, v); }
//讀取PCI配置空間中特定位置的配置值
static uint32_t pci_conf_read(struct pci_func *f, uint32_t off) { pci_conf1_set_addr(f->bus->busno, f->dev, f->func, off); return inl(pci_conf1_data_ioport); } //設定PCI配置空間中特定位置的配置值 static void pci_conf_write(struct pci_func *f, uint32_t off, uint32_t v) { pci_conf1_set_addr(f->bus->busno, f->dev, f->func, off); outl(pci_conf1_data_ioport, v); }

初始化PCI的大致流程:

在pci_init函式中,root_bus被全部清0,然後交給pci_scan_bus函式來掃描這條總線上的所有裝置,說明在JOS中E1000網絡卡是連線在0號總線上的。pci_scan_bus函式來順次查詢0號總線上的32個裝置,如果發現其存在,那麼順次掃描它們每個功能對應的配置地址空間,將一些關鍵的控制引數讀入到pci_func中進行儲存。得到pci_func函式後,被傳入pci_attach函式去查詢是否為已存在的裝置,並用相應的初始化函式來初始化裝置。

在手冊的5.2節中可以找到venderID和deviceID:

82540EM-A 8086h 100E Desktop(桌上型電腦)

在kern/ PCI.c中的pci_attach_vendor陣列中新增一個條目,以便在找到匹配的PCI裝置時觸發函式(請確保將它放在表示 末尾的{0,0,0}條目之前)

struct pci_driver pci_attach_vendor[] = {
    { PCI_VENDOR_ID, PCI_DEVICE_ID, &e1000_init },
    { 0, 0, 0 },
};

前兩個引數PCI_VENDOR_ID, PCI_DEVICE_ID的定義放在kern/pcireg.h檔案中,這個檔案是用來存放PCI的配置資訊的

#define PCI_VENDOR_ID                           0x8086
#define PCI_DEVICE_ID                           0x100E

第三個引數應該是他初始化的時候應該呼叫的函式,可以定義在kern/e1000.c中

int
e1000_init(struct pci_func *pcif)
{
        pci_func_enable(pcif);
        return 1;
}

然後在kern/e1000.h中宣告這個函式,讓pic.c進行呼叫

#include <kern/pci.h>

int e1000_init(struct pci_func *pcif);

exercise 4 :

在kern/e1000.c中的函式新增:

int
e1000_init(struct pci_func *pcif)
{
        pci_func_enable(pcif);
        pci_e1000 = mmio_map_region(pcif->reg_base[0], pcif->reg_size[0]);
        return 1;
}

我們使用變數pci_e1000來記錄對映的位置,以便稍後訪問剛才對映的暫存器。

如果要嘗試列印裝置狀態暫存器

在QEMU's e1000_hw.h 中有關於狀態暫存器的定義。

#define E1000_STATUS   0x00008  /* Device Status - RO */

在kern/e1000.h中新增

#define E1000_STATUS   0x00008  /* Device Status - RO */
#define e1000_print_status(offset) \
        cprintf("the E1000 status register: [%08x]\n", *(pci_e1000+(offset>>2)));
// 由於pci_e1000是uint32_t的,如果直接加offset,就相當於加了offset*sizeof(uint32_t)
// 例如the E1000:[ef804000] offset:[00000008] sum:[ef804020]
// 並且定義
pci_e1000
uint32_t *pci_e1000;

修改

int
e1000_init(struct pci_func *pcif)
{
        pci_func_enable(pcif);
        pci_e1000 = mmio_map_region(pcif->reg_base[0], pcif->reg_size[0]);
        e1000_print_status(E1000_STATUS);
        return 1;
}

exercise5:

在kern/e1000.c中:

int
e1000_init(struct pci_func* pcif)
{
    pci_func_enable(pcif);
    //pci_e1000 是1個指標,指向對映地址, uint32_t* pci_e1000;
    //他建立了一個虛擬記憶體對映
    pci_e1000 = mmio_map_region(pcif->reg_base[0], pcif->reg_size[0]);
    e1000_print_status(E1000_STATUS);
    
    //清零兩個陣列
    memset(tx_list, 0, sizeof(struct tx_desc) * TX_MAX);
    memset(tx_buf, 0, sizeof(struct packets) * TX_MAX);
    //每一個傳輸描述符都對應著一個packet
    for (int i = 0; i < TX_MAX; i++) {
        tx_list[i].addr = PADDR(tx_buf[i].buffer); //不太懂為什麼可以用PADDR
        //24=4(16進位制相對於2進位制)*6
        tx_list[i].cmd = (E1000_TXD_CMD_RS >> 24) | (E1000_TXD_CMD_EOP >> 24);
        //因為RSV除了82544GC/EI之外,所有乙太網控制器都應該保留這個位,並將其程式設計為0b。
        //而LC,EC在全雙工沒有意義,
        tx_list[i].status = E1000_TXD_STAT_DD;
    }
    //(TDBAL/TDBAH)指向傳輸描述符佇列的base和high
    //將pci_e1000視為陣列的話,他存放的元素應該是32位的,
    //乙太網控制器中的暫存器都是32位的,
    pci_e1000[E1000_TDBAL >> 2] = PADDR(tx_list);
    pci_e1000[E1000_TDBAH >> 2] = 0;
    pci_e1000[E1000_TDT >> 2] = 0;
    pci_e1000[E1000_TDLEN >> 2] = TX_MAX * sizeof(struct tx_desc);
    pci_e1000[E1000_TDH >> 2] = 0;
    //pci_e1000[E1000_TDT >> 2] = 0;
    pci_e1000[E1000_TCTL >> 2] |= (E1000_TCTL_EN | E1000_TCTL_PSP |
        (E1000_TCTL_CT & (0x10 << 4)) |
        (E1000_TCTL_COLD & (0x40 << 12)));
    pci_e1000[E1000_TIPG >> 2] |= (10) | (4 << 10) | (6 << 20);


    return 1;
}

在kern/e1000.h中加入:

#include <inc/string.h>


//以下定義來自  QEMU's e1000_hw.h
#define E1000_TCTL     0x00400  /* TX Control - RW */
#define E1000_TDBAL    0x03800  /* TX Descriptor Base Address Low - RW */
#define E1000_TDBAH    0x03804  /* TX Descriptor Base Address High - RW */
#define E1000_TDLEN    0x03808  /* TX Descriptor Length - RW */
#define E1000_TDH      0x03810  /* TX Descriptor Head - RW */
#define E1000_TDT      0x03818  /* TX Descripotr Tail - RW */
#define E1000_TIPG     0x00410  /* TX Inter-packet gap -RW */
#define E1000_TCTL_EN            0x00000002    /* enable tx */
#define E1000_TCTL_BCE           0x00000004    /* busy check enable */
#define E1000_TCTL_PSP           0x00000008    /* pad short packets */
#define E1000_TCTL_CT            0x00000ff0    /* collision threshold */
#define E1000_TCTL_COLD          0x003ff000    /* collision distance */

#define E1000_TXD_CMD_RS         0x08000000     /* Report Status */
#define E1000_TXD_CMD_EOP    0x01000000 /* End of Packet */

#define E1000_TXD_STAT_DD        0x00000001     /* Descriptor Done */


#define TX_MAX         64
//legacy transmit descriptor format
struct tx_desc
{
    uint64_t addr;   //Address of the transmit descriptor in the host memory
    uint16_t length; //The Checksum offset field indicates where to insert a TCP checksum if this mode is enabled. 
    uint8_t cso;
    uint8_t cmd;
    uint8_t status;
    uint8_t css; //The Checksum start field (TDESC.CSS) indicates where to begin computing the checksum. 
    uint16_t special;
}__attribute__((packed));//告訴編譯器取消結構在編譯過程中的優化對齊,按照實際佔用位元組數進行對齊,

//傳輸描述符陣列
struct tx_desc tx_list[TX_MAX];
//乙太網資料包的最大大小為1518位元組,將其限制在2048方便對齊

struct packets {
    char buffer[2048];
}__attribute__((packed));
//緩衝區陣列
struct packets tx_buf[TX_MAX];

執行make E1000_DEBUG=TXERR,TX qemu

在設定TDT register時,應該會看到一條“e1000: tx disabled”訊息(因為這是在設定TCTL.EN之前發生的),並且不再有“e1000”訊息

exercise 6:

在kern/e1000.c中加入

int
e1000_transmit(void* addr, int length) {
    //TDT是傳輸描述符陣列的索引
    int tail = pci_e1000[E1000_TDT >> 2];
    //得到下一個描述符
    struct tx_desc* tx_next = &tx_list[tail];
    //如果包長度超出了,就直接截住
    if (length > sizeof(struct packets))
        length = sizeof(struct packets); 
    //如果DD位被設定了,就說明這個傳輸描述符安全回收,可以傳輸下一個包,也就是佇列還沒有滿
    if ((tx_next->status & E1000_TXD_STAT_DD) == E1000_TXD_STAT_DD) {
        //沒有滿,把包複製到緩衝區裡面去
        memmove(KADDR(tx_next->addr), addr, length);
        tx_next->status &= !E1000_TXD_STAT_DD; //表示現在該描述符還沒被處理
        tx_next->length = (uint16_t)length;
        //更新TDT,注意是迴圈佇列
        pci_e1000[E1000_TDT >> 2] = (tail + 1) % TX_MAX;  
        cprintf("my message:%s, %d, %02x\n", tx_buf[tail].buffer, tx_list[tail].length, tx_list[tail].status);
        return 0;
    }
    //DD位沒有被設定,傳輸佇列滿了
    return -1;
}

在kern/e1000.h中宣告這個函式

在kern/monitor.c中新增kern/e1000.h標頭檔案

然後:

void
monitor(struct Trapframe *tf)
{
    char *buf;
    cprintf("Welcome to the JOS kernel monitor!\n");
    cprintf("Type 'help' for a list of commands.\n");
    e1000_transmit("I'm here", 10);
    if (tf != NULL)
        print_trapframe(tf);

    while (1) {
        buf = readline("K> ");
        if (buf != NULL)
            if (runcmd(buf, tf) < 0)
                break;
    }
}

在qemu中測試:

make E1000_DEBUG=TXERR,TX qemu

能得到以下結果:

e1000: index 0: 0x2b02c0 : 900000a 0
my message:I'm here, 10, 00

在qemu中測試:

 tcpdump -XXnr qemu.pcap

得到以下結果:

reading from file qemu.pcap, link-type EN10MB (Ethernet)
16:09:40.990305 [|ether]
    0x0000:  4927 6d20 6865 7265 004b                 I'm.here.K

exercise 7:

在kern/syscall.c中新增系統呼叫函式

//network packet send
static int
sys_packet_try_send(void *addr, size_t len){
        user_mem_assert(curenv, addr, len, PTE_U); //考慮沒這麼周全
        return e1000_transmit(addr, len);
}

在diapstach中新增分支:

case (SYS_packet_try_send):
                return sys_packet_try_send((void *)a1,a2);

新增宣告和定義:

//在kern/syscall.c中
#include<kern/e1000.h>

//lib/syscall.c中
int 
sys_packet_try_send(void* buf, size_t len)
{
    return syscall(SYS_packet_try_send, 0,(uint32_t)buf, len, 0,0,0);
}

//inc/syscall.h中要宣告
SYS_packet_try_send,

//inc/lib.h中
intsys_packet_try_send(void *data_va,size_t len);

exercise 8:

# include <inc/lib.h>

void
output(envid_t ns_envid)
{  
        binaryname = "ns_output";
        int r;          
        int perm;
        // LAB 6: Your code here:
        //      - read a packet from the network server
        //      - send the packet to the device driver
        envid_t from_env;
        while(1){       
                if( ipc_recv(&from_env, &nsipcbuf, &perm) != NSREQ_OUTPUT)
                        continue;
                while((r = sys_packet_try_send(nsipcbuf.pkt.jp_data, nsipcbuf.pkt.jp_len)<0))
                        sys_yield();
        }       
}  

執行 make grade 可以看到part a的測試全部通過