LoginSignup
7
9

More than 5 years have passed since last update.

QEMUに仮想PCIデバイスを追加する(8) : DMAの設定

Last updated at Posted at 2015-01-04

DMA 転送について

DMA 転送をする際にはデバイスは物理アドレスではなくバスアドレスを必要とします。
これは、プラットフォームによっては物理アドレスをIOMMUを用いて更にバスアドレスへ
変換しているためです。

また、PCIデバイスではデバイス側がDMA転送のマスタとなることができ、
転送の開始をデバイス側で始められます(恐らく通常この方法です)。
今回もこの方法を採用し、ドライバはDMA転送開始をioctlで
仮想デバイスへ伝え、仮想デバイス側がバスからデータをやりとりします。

consistent DMA/ streaming DMA

DMA においてはメモリとデバイスで直接データをやり取りするため、CPU内の
キャッシュ一貫性を保つ必要が出てきます。
どの様にして一貫性を保つかに応じて consistent (coherent) DMA と streaming DMAの2種類があります。
consistent DMA は常にキャッシュ一貫性のあるメモリ領域に確保され
(つまりキャッシュを利用しない?)、扱いは簡単ですが速度が犠牲となります。
一方streaming DMAは転送時にのみ領域を確保、キャッシュ一貫性を保証するために
領域確保後はドライバがバッファにアクセスすることはできません(領域を登録解除してからアクセスする)。

頻繁にデータをやり取りする場合に consistent DMA、一方的なデータを
送るためには streaming DMA を用いるようです。

ここでは両方をテストしています。
ユーザーがioctlを用いてDMA転送を指示すると、
ドライバはDMA 転送処理を行います。
デバイスはDMA転送開始の write 信号を受けとった後、
consistent DMA では受け取ったデータを+1, streaming DMA では
ソートをした後にDMA転送仕返し、割り込みを発生させています。

consistent DMA ではDMA転送完了を確実にするためユーザープログラム
が単純にスリープしています。一方で streaming DMA では
デバイスからのDMA転送完了を待つ waitqueue をドライバが使用しています。

追記 : 恐らくQEMUでは仮想デバイスの関数が終わるまで
他の処理に映らないため、上のようなデバイスの完了を待つ処理は
無意味です。

QEMU仮想デバイス

include/sysemu/dma.h に pci_dma_read、 pci_dma_write といった関数があるので、
ドライバからDMA開始要求が来た時にこれを利用します。
予めDMAのバスアドレスやデータ長を知っておく必要があるので、
これらのデータを事前にデバイスドライバがセットするようにします。

    void test_show_cdmabuf(TestPCIState *s) 
    {
        PCIDevice *pci_dev = PCI_DEVICE(s);
        int i;

        for (i = 0; i < TEST_CDMA_BUFFER_NUM; i++) {
            printf("%3d ", s->cdma_buf[i]++);
        }
        printf("\n");

        pci_dma_write(pci_dev,  s->cdma_addr, s->cdma_buf, s->cdma_len);

        s->intmask |= INT_CDMA;
        pci_irq_assert(pci_dev);
    }

    void test_show_sdmabuf(TestPCIState *s) 
    {
        PCIDevice *pci_dev = PCI_DEVICE(s);
        int i;

        for (i = 0; i < TEST_SDMA_BUFFER_NUM; i++) {
            printf("%3d ", s->sdma_buf[i]);
        }
        printf("\n");
        qsort(s->sdma_buf, TEST_SDMA_BUFFER_NUM, sizeof(int), comp_int);
        pci_dma_write(pci_dev,  s->sdma_addr, s->sdma_buf, s->sdma_len);

        s->intmask |= INT_SDMA;
        pci_irq_assert(pci_dev);
    }

// test_pci_pio_write 中
    swith(addr) {
        /* ...*/

        // coherent DMA
        case TEST_SET_CDMA_ADDR:
            s->cdma_addr = val;
            break;
        case TEST_SET_CDMA_LEN:
            s->cdma_len = val;
            break;
        case TEST_CDMA_START:
            pci_dma_read(pdev, s->cdma_addr, s->cdma_buf, s->cdma_len);
            test_show_cdmabuf(s);
            break;

        // streaming DMA
        case TEST_SET_SDMA_ADDR:
            s->sdma_addr = val;
            break;
        case TEST_SET_SDMA_LEN:
            s->sdma_len = val;
            break;
        case TEST_SDMA_START:
            pci_dma_read(pdev, s->sdma_addr, s->sdma_buf, s->sdma_len);
            test_show_sdmabuf(s);
            break;

        /* ...*/
    }

デバイスドライバ

DMA の初期化は test_pci_probe 中で 行っています。
PCI 用のDMA初期化の方法があるので、それに沿っています。

// test_pci_probe 中
        err = pci_set_dma_mask(pdev, DMA_BIT_MASK(32));
        if(err) {
            printk(KERN_ERR "Cannot set DMA mask\n");
            goto error;
        }
        pci_set_master(pdev);

        // allocate consistent DMA
        dev_data->cdma_buffer = pci_alloc_consistent(pdev, TEST_CDMA_BUFFER_SIZE, &dev_data->cdma_addr);
        if(dev_data->cdma_buffer == NULL) {
            printk(KERN_ERR "Cannot allocate consistent DMA buffer\n");
            goto error;
        }
        dev_data->cdma_len = TEST_CDMA_BUFFER_SIZE;

        // send consistent DMA info to device
        outl(dev_data->cdma_addr, dev_data->pio_base + TEST_SET_CDMA_ADDR);
        outl(dev_data->cdma_len,  dev_data->pio_base + TEST_SET_CDMA_LEN);

        tprintk("cdma_addr : %x\n",  dev_data->cdma_addr);

        // streaming DMA
        dev_data->sdma_buffer = kmalloc(TEST_SDMA_BUFFER_SIZE, GFP_KERNEL);
        if(dev_data->sdma_buffer == NULL) {
            printk(KERN_ERR "Cannot allocate streaming DMA buffer\n");
            goto error;
        }
        init_waitqueue_head(&(dev_data->sdma_q));

// test_pci_remove 中
    pci_free_consistent(dev_data->pdev, TEST_CDMA_BUFFER_SIZE,
            dev_data->cdma_buffer, dev_data->cdma_addr);

まず初めに pci_set_dma_mask でPCIバス幅をセットしています。
次に pci_set_master を呼び、PCIバスマスタ機能をオンにします。

次に consistent DMA の設定を行っています。
pci_alloc_consistent によりDMAバッファを確保しています。
戻り値(dev_data->cdma_buffer) がドライバが使用するデータ領域で、
第四引数(dev_data->cdma_addr) がデバイスへ渡すバスアドレスとなります。
このバッファは終了時にpci_free_consistent を用いて開放を行います。

streaming DMA の方は kmalloc を使って ドライバが使用する領域
のみを確保しています。また、waitqueue を初期化しています。

なお、pci_alloc_consistent などの関数は dma_alloc_consistent といった関数の
ラッパーになっています。

実際のDMA転送の発行はioctl 内で行っています。

// test_pci_handler 中
        if(intmask & INT_CDMA) {
        }
        if(intmask & INT_SDMA) {
            sdma_done = 1;
        }


// test_pci_uioctl 中
            case TEST_CMD_CDMA_START :
                copy_from_user(dev_data->cdma_buffer, d->cdmabuf, TEST_CDMA_BUFFER_SIZE);
                wmb();
                outl(CDMA_START, dev_data->pio_base + TEST_CDMA_START);
                break;
            case TEST_CMD_GET_CDMA_DATA :
                n = copy_to_user(d->cdmabuf, dev_data->cdma_buffer, TEST_CDMA_BUFFER_SIZE);
                if(n != 0){
                    tprintk("cannot copy all data (%d bytes in %d bytes)\n", n, TEST_CDMA_BUFFER_SIZE);
                }
                break;

            case TEST_CMD_SDMA_START :
                copy_from_user(dev_data->sdma_buffer, d->sdmabuf, TEST_SDMA_BUFFER_SIZE);
                dev_data->sdma_addr = pci_map_single(dev_data->pdev, dev_data->sdma_buffer,
                        TEST_SDMA_BUFFER_SIZE, DMA_BIDIRECTIONAL);
                dev_data->sdma_len = TEST_SDMA_BUFFER_SIZE;
                wmb();
                outl(dev_data->sdma_addr, dev_data->pio_base + TEST_SET_SDMA_ADDR);
                outl(dev_data->sdma_len, dev_data->pio_base + TEST_SET_SDMA_LEN);
                wmb();
                outl(SDMA_START, dev_data->pio_base + TEST_SDMA_START);
                break;
            case TEST_CMD_GET_SDMA_DATA :
                tprintk("sdma_done %d\n", sdma_done);
                if(wait_event_interruptible(dev_data->sdma_q, (sdma_done == 1))) {
                        return -ERESTARTSYS;
                }
                sdma_done = 0;

                pci_unmap_single(dev_data->pdev, dev_data->sdma_addr,
                        TEST_SDMA_BUFFER_SIZE, DMA_BIDIRECTIONAL);
                n = copy_to_user(d->sdmabuf, dev_data->sdma_buffer, TEST_SDMA_BUFFER_SIZE);
                if(n != 0) {
                    tprintk("cannot copy all data (%d bytes in %d bytes)\n", n, TEST_SDMA_BUFFER_SIZE);
                }
                break;

ここで、wmb() はライトバリアでありその呼出しの前の書き込み処理が必ず完了することを
保証します(リードバリアはrmb(), 両方の場合はmb())。
これによりコンパイラの最適化やプロセッサのアウトオブオーダー実行で
命令の順序が変更されることを防ぎます。

consistent DMA の場合はユーザーからデータを受け取った後にout命令で
実行開始をデバイスへ伝えています。

一方でstreaming DMA の場合は転送前にDMA領域をマップするといった処理が必要となります。
これには、pci_map_singleを用いています。第四引数がデータの移動方向で DMA_TO_DEVICE, DMA_FROM_DEVICE,
DMA_BIDIRECTINAL から選択します。
名前の通り双方向、デバイスへの一方向、デバイスからの一方向となります。
今回は簡単のためにDMA_BIDREDCTINAL を選択していますが、
実際にはDMA_TODEVICE か DMA_FROMDEVICE を選択して一方向のみの通信として利用するべきです。

なぜなら、デバイスが直接アクセスできない領域にDMAバッファが作成された場合、アーキテクチャに
よっては別のデバイスがアクセス可能な領域にそのバッファ(bounce buffer)がコピーされるのですが、
BIDIRECTIONAL の場合は転送前、後ともにバッファのコピーが
(CPU を通じて)行われる可能性があり、結果として速度が犠牲になるからです。

streaming DMA においてデバイスからの転送結果を獲得するためには
pci_unmap_single を用いてDMA 領域をアンマップします。
今回は streaming DMA からのデータ取得には waitqueue を使用して、
sdma_done が1となるまで TEST_CMD_GET_SDMA_DATA の処理をスリープさせます
(wait_event_interruptible)。sdma_doneは
割り込みハンドラ中で 1 にします。

ユーザプログラム

ioctlシステムコールを使用します。 consistent DMA の場合は
DMA転送が必ず完了することを保証するためにsleep(1)を入れています
(全く良くない方法です)。

バッファ領域をconsistent DMA では 添字=データ、 streaming DMAでは
0-999 の乱数で初期化しています。

void cdma_test(int fd)
{
    int i;

    test_ioctl_data *d;
    d = malloc(sizeof(test_ioctl_data));

    if(!d) exit(1);
    printf("\n---- start consistent dma test ----\n");

    for(i = 0; i < TEST_CDMA_BUFFER_NUM; i++){
        d->cdmabuf[i] = i;
    }
    ioctl(fd, TEST_CMD_CDMA_START, d);

    sleep(1);
    ioctl(fd, TEST_CMD_GET_CDMA_DATA, d);
    for(i = 0; i < TEST_CDMA_BUFFER_NUM; i++){
        printf("%3d ", d->cdmabuf[i]);
    }
    printf("\n");

    printf("\n---- end consistent dma test ----\n");

    free(d);
}

void sdma_test(int fd)
{
    int i;

    test_ioctl_data *d;
    d = malloc(sizeof(test_ioctl_data));

    if(!d) exit(1);

    srand(123);
    printf("\n---- start streaming dma test ----\n");

    for(i = 0; i < TEST_SDMA_BUFFER_NUM; i++){
        d->sdmabuf[i] = rand()%1000;
    }
    ioctl(fd, TEST_CMD_SDMA_START, d);

    ioctl(fd, TEST_CMD_GET_SDMA_DATA, d);
    for(i = 0; i < TEST_SDMA_BUFFER_NUM; i++){
        printf("%3d ", d->sdmabuf[i]);
    }
    printf("\n");

    printf("\n---- end streaming dma test ----\n");

    free(d);
}

実行結果

実行結果は以下のようになります。

---- start consistent dma test ----
** (151) test_pci_uioctl     : _cmd:1074035460
## (155) test_pci_pio_write  : addr 112, size 4
  0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
** (269) test_pci_handler    : irq handler called
## (118) test_pci_pio_read   : addr 21, size 1
## (155) test_pci_pio_write  : addr 20, size 1
## (155) test_pci_pio_write  : addr 104, size 4
** (151) test_pci_uioctl     : _cmd:-2147190011
  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256

---- end consistent dma test ----

---- start streaming dma test ----
** (151) test_pci_uioctl     : _cmd:1074035462
## (155) test_pci_pio_write  : addr 56, size 4
## (155) test_pci_pio_write  : addr 60, size 4
## (155) test_pci_pio_write  : addr 52, size 4
393  13 873 630 279 331 195  22 426   1 824 668 921 291 316 381 709 207 646 993 926 365 699 991 976 878 116 148 255 372 485 648 385 358 631  17 689 826  39 116 180 215 784 101 507 452 834 568  11 480 913 289 198 613 281 174 843 397 674 451 769 159  99 507 869  82 524 559 909 563  27 441 779 163 542 638 615 728 558 626 560 471 916 758  84 549 284 280 946 958 731  68 469 182 575 339 265  99 898 174  14 925 967 145  88 509 135  55 589 693  33 149 165 949 908 601 850 192 881 797 503 612 865 972 147 792 311 412 891 561 938 257 838 905 403 278 766 538 333 355 584 367 504 101 668 764 702 519 309 936 668 812 900 885 136  47 677 800 459 568 361 397 177 552 654 580 830 420 471 516 775  55 235 632 156 903 396 210 774  57 498 794 221 399 679 358 798 356 510 610 276 871 359 454 775  14 386 606 434 209 474 562 264 709 194 772 964 942 335 739   0 833 885 573 584 565 931 383 273 441 993 550 665 704 356 792 718  94 398 153 304 872  67 568 933 261 693 898 203  28 989 555 861 874 129 798 439 412 181  65 206 526 615 223 582 323
** (269) test_pci_handler    : irq handler called
## (118) test_pci_pio_read   : addr 21, size 1
## (155) test_pci_pio_write  : addr 20, size 1
## (155) test_pci_pio_write  : addr 104, size 4
** (151) test_pci_uioctl     : _cmd:-2147190009
** (191) test_pci_uioctl     : sdma_done 1
  0   1  11  13  14  14  17  22  27  28  33  39  47  55  55  57  65  67  68  82  84  88  94  99  99 101 101 116 116 129 135 136 145 147 148 149 153 156 159 163 165 174 174 177 180 181 182 192 194 195 198 203 206 207 209 210 215 221 223 235 255 257 261 264 265 273 276 278 279 280 281 284 289 291 304 309 311 316 323 331 333 335 339 355 356 356 358 358 359 361 365 367 372 381 383 385 386 393 396 397 397 398 399 403 412 412 420 426 434 439 441 441 451 452 454 459 469 471 471 474 480 485 498 503 504 507 507 509 510 516 519 524 526 538 542 549 550 552 555 558 559 560 561 562 563 565 568 568 568 573 575 580 582 584 584 589 601 606 610 612 613 615 615 626 630 631 632 638 646 648 654 665 668 668 668 674 677 679 689 693 693 699 702 704 709 709 718 728 731 739 758 764 766 769 772 774 775 775 779 784 792 792 794 797 798 798 800 812 824 826 830 833 834 838 843 850 861 865 869 871 872 873 874 878 881 885 885 891 898 898 900 903 905 908 909 913 916 921 925 926 931 933 936 938 942 946 949 958 964 967 972 976 989 991 993 993

---- end streaming dma test ----

はじめ: (1) 作ったもの

前: (7) 割り込みの設定

7
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
9