一款DMA性能優化記錄:異步傳輸和指定實時信號做async IO【轉】

轉自:https://www.cnblogs.com/arnoldlu/p/10219704.html

DMA本身用於減輕CPU負擔,進行CPU off-load搬運工作。

在DMA驅動內部實現有同步和異步模式,異步模式使用dma_async_issue_pending(),然後在callback()中發送SIGIO信號,用戶空間收到SIGIO進行handler處理視爲一個週期完成。

同步模式,採用dma_sync_wait()進行等待,期間並沒有釋放CPU給其他進程使用。

在一個項目中,發現DMA相關佔用率高的問題,後來發現是因爲其使用了同步模式。然後,將其改成異步模式,並對其進行詳細的分析,記錄如下。

 

那麼當初爲什麼沒有使用async IO模式呢?

原來是因爲同一進程中其他線程也是用了async IO設備,由於kill_fasync()發送SIGIO信號,在一個進程內無法多個handler。

權宜措施使用了同步DMA,造成佔用率高。

然後通過fcntl(fd, F_SETSIG, sig);解決了kill_fasync()發送相同SIGIO信號的衝突問題。

1. 問題發現:DMA同步模式佔用率高

抓數據命令:

perf record -a -e cpu-clock  -- sleep 60
perf report
perf report -s comm

不同數據量,不同CMA處理方式下的top和perf結果:

Item 1 cma    per cma
top perf top perf
4K 25fps 15%   84.43% swapper [kernel.kallsyms] [k] __sched_text_end
11.51% main [kernel.kallsyms] [k] dma_cookie_status
2.81% main [kernel.kallsyms] [k] dma_sync_wait
0.20% main [kernel.kallsyms] [k] uart_write
0.13% swapper [kernel.kallsyms] [k] cache_op_range
0.12% swapper [kernel.kallsyms] [k] __dma_tx_complete
0.06% swapper [kernel.kallsyms] [k] dw_dma_tasklet
0.04% ksoftirqd/0 [kernel.kallsyms] [k] finish_task_switch
0.03% perf [kernel.kallsyms] [k] raw_copy_from_user
0.02% swapper [kernel.kallsyms] [k] __softirqentry_text_start
 61% 45.93% main [kernel.kallsyms] [k] dcache_wb_line
36.79% swapper [kernel.kallsyms] [k] __sched_text_end
4.86% main [kernel.kallsyms] [k] dma_cookie_status
4.06% main [kernel.kallsyms] [k] free_hot_cold_page
1.28% main [kernel.kallsyms] [k] dma_sync_wait
1.28% main [kernel.kallsyms] [k] skip_ftrace
0.66% main [kernel.kallsyms] [k] _mcount
0.58% main [kernel.kallsyms] [k] unset_migratetype_isolate
0.51% main [kernel.kallsyms] [k] __free_pages
0.41% main [kernel.kallsyms] [k] start_isolate_page_range
1080p 50fps 9% 89.82% swapper [kernel.kallsyms] [k] __sched_text_end
5.63% main [kernel.kallsyms] [k] dma_cookie_status
1.48% main [kernel.kallsyms] [k] dma_sync_wait
0.49% ksoftirqd/0 [kernel.kallsyms] [k] finish_task_switch
0.24% swapper [kernel.kallsyms] [k] dw_dma_tasklet
0.13% swapper [kernel.kallsyms] [k] __dma_tx_complete
0.10% swapper [kernel.kallsyms] [k] tasklet_action
0.10% main [kernel.kallsyms] [k] do_futex
0.06% main [kernel.kallsyms] [k] raw_copy_from_user
0.06% main [kernel.kallsyms] [k] restore_from_user_fp
44%  53.49% swapper [kernel.kallsyms] [k] __sched_text_end
30.73% main [kernel.kallsyms] [k] dcache_wb_line
3.65% main [kernel.kallsyms] [k] free_hot_cold_page
3.36% main [kernel.kallsyms] [k] dma_cookie_status
1.03% main [kernel.kallsyms] [k] skip_ftrace
0.85% main [kernel.kallsyms] [k] dma_sync_wait
0.56% main [kernel.kallsyms] [k] _mcount
0.48% main [kernel.kallsyms] [k] unset_migratetype_isolate
0.38% main [kernel.kallsyms] [k] __free_pages
0.33% main [kernel.kallsyms] [k] isolate_migratepages_range

問題的分析:

1. skip_trace和_mcount兩個是因爲ftrace引入的負荷,不合理。此時不應該有這些。----需要推動csky修改成Dynamic function/function_graph。

2. dcache_wb_line/free_hot_cold_page是CMA操作引起的。-----------------------------------這裏需要修改處理方式,CMA不需要重複申請釋放。

3. dma_cookie_status/dma_sync_wait是DMA操作引起的。------------------------------------這裏可以通過修改DMA異步信號觸發來降低佔用率。

分析總結:

1. 從上面的測試結果看,應該儘量避免內存申請釋放。csky內存處理效率很低。

2. 同步模式效率非常低,4K單路佔用率達到15%,如果4K雙路就沒法使用了。

所以必須要使用異步模式,這也是使用DMA的初衷。

1.1 不同DMA size對性能影響

多大的傳輸使用DMA獲得的收益最高呢?做了個實驗,結果如下,橫軸單位是B,縱軸單位是MB/s、

可以看出,DMA size大小越大效率越高,size接近3MB的時候,以及以後吞吐率就比較穩定了。

2. 分析問題:讓DMA異步起來

複製代碼
 
int axidma_memcpy(dma_addr_t src, dma_addr_t dst, unsigned int len)
{
    struct dma_async_tx_descriptor *tx = NULL;
    dma_cookie_t        cookie;
    unsigned long  flags;
    bool sync_wait = false;-------------------------------------------------------------------true表示同步等待模式;false表示異步模式,和callback()配合。
    int err = 0;
    
    flags = DMA_CTRL_ACK | DMA_PREP_INTERRUPT;

    tx = xxxxxx->dma.chan->device->device_prep_dma_memcpy(xxxxxx->dma.chan, dst, src, len, flags);
    if (!tx)
    {
        pr_err("Fail to prepare memcpy.\n");
        return -1;
    }

    tx->callback = axidma_callback;
    tx->callback_param = xxxxxx;
    cookie = tx->tx_submit(tx);
    if (dma_submit_error(cookie))
    {
        pr_err("Fail to submit axi dma.\n");
        return -1;
    }
    if (!sync_wait) {
        dma_async_issue_pending(dma_dev->dma.chan);------------------------------------------發送DMA傳輸請求,然後退出。這裏不會等待操作結果。
    } else {
        if (dma_sync_wait(dma_dev->dma.chan, cookie) == DMA_COMPLETE) {
            err = 0;
        } else {
            err = -EIO;
        }
    }

    return err;
}


enum dma_status dma_sync_wait(struct dma_chan *chan, dma_cookie_t cookie)
{
    enum dma_status status;
    unsigned long dma_sync_wait_timeout = jiffies + msecs_to_jiffies(5000);-------------------超時5000ms,足夠大了。

    dma_async_issue_pending(chan);------------------------------------------------------------和之前同樣功能,發送DMA傳輸請求。只是下面會進行等待,並有超時動作。
    do {
        status = dma_async_is_tx_complete(chan, cookie, NULL, NULL);--------------------------pool DMA傳輸狀態。
        if (time_after_eq(jiffies, dma_sync_wait_timeout)) {
            dev_err(chan->device->dev, "%s: timeout!\n", __func__);
            return DMA_ERROR;-----------------------------------------------------------------超時退出。
        }
        if (status != DMA_IN_PROGRESS)
            break;
        cpu_relax();--------------------------------------------------------------------------讓出CPU執行。
    } while (1);

    return status;
}

static inline enum dma_status dma_async_is_tx_complete(struct dma_chan *chan,
    dma_cookie_t cookie, dma_cookie_t *last, dma_cookie_t *used)
{
    struct dma_tx_state state;
    enum dma_status status;

    status = chan->device->device_tx_status(chan, cookie, &state);
    if (last)
        *last = state.last;
    if (used)
        *used = state.used;
    return status;
}
複製代碼

然後在callback()中發送SIGIO信號:

複製代碼
static void axidma_callback(void *arg)
{
    if(dma_dev->async)
    {
        pr_debug("axidma callback: chan=%s.\n", dma_chan_name(dma_dev->dma.chan));
        pr_debug("axidma_callback: magic=0x%08x pid_type=%d\n", dma_dev->async->magic, dma_dev->async->fa_file->f_owner.pid_type);
        kill_fasync(&dma_dev->async, SIGIO, POLL_IN);--------------------------------在傳輸完成後,異步發送SIGIO信號。
    }
}
複製代碼

3. 解決問題:DMA異步傳輸

 爲了排除其他進程影響,單獨構造3個試用例:1. 4K 25fps;2. 1080p 2路 25fps;3. 1080p 4路 25fps。

 然後改成DMA異步模式,CPU佔用率是否明顯下降?

測試程序如下:

複製代碼
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <pthread.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include "IFMS_MemCMA_API.h"
 
int fd;
unsigned int handle_count = 0;
volatile unsigned int dmatest_exit = 0;
volatile sigio_received = 0;
#define LOOP_COUNT 10000
#define DMA_MEMCPY    0
#define DMA_FILE "/dev/dpeye1000_axidma"

#define DPEYE1000_AXIDMA_IOC_MAGIC  'd'
#define DPEYE1000_AXIDMA_REQUEST_CHNN   _IOW(DPEYE1000_AXIDMA_IOC_MAGIC, 1, int)    //Request channel.
#define DPEYE1000_AXIDMA_RELEASE_CHNN   _IOW(DPEYE1000_AXIDMA_IOC_MAGIC, 2, int)    //Release channel.
#define DPEYE1000_AXIDMA_MEMCPY         _IOW(DPEYE1000_AXIDMA_IOC_MAGIC, 3, int)    //Do 1d memcpy.

//#define PERFORMANCE_DEBUG

struct axidma_dma
{
    struct dma_chan *chan;

    unsigned long    src;
    unsigned long    dst;

    // for memcpy
    unsigned int      len;
    unsigned int    n_channels;
};

struct thread_para
{
    unsigned int size;
    unsigned int expries;
    struct cma_block cma_block1;
    struct cma_block cma_block2;
};

static struct axidma_dma axidma_dma;
struct thread_para t_para;
int dmacopy_tid = 0;

void trigger_dma_copy(void)
{
    int ret;
    #ifdef PERFORMANCE_DEBUG
    struct timespec time_0, time_1;
    unsigned long duration;
    float throughput = 0.0;

    clock_gettime(CLOCK_REALTIME, &time_0);
    #endif

    axidma_dma.src = (unsigned long)t_para.cma_block1.addr_p;
    axidma_dma.dst = (unsigned long)t_para.cma_block2.addr_p;
    axidma_dma.len = t_para.size;
    ret = ioctl(fd, DPEYE1000_AXIDMA_MEMCPY, &axidma_dma);
    if(ret < 0)
    {
        printf("fail to ioctl DPEYE1000_AXIDMA_MEMCPY!!!\n");
    }
    #ifdef PERFORMANCE_DEBUG
    clock_gettime(CLOCK_REALTIME, &time_1);

    duration = (time_1.tv_sec-time_0.tv_sec)*1000000000 + (time_1.tv_nsec-time_0.tv_nsec);
    throughput = (float)t_para.size/1048576*1000000000/duration;
    printf("%ld.%ld %d %d, %ld, %f\n", time_1.tv_sec, time_1.tv_nsec, handle_count, t_para.size, duration, throughput);
    #endif
}

void sigint_handler(int sig)
{
    if(sig == SIGINT)
    {
        printf("%s SIGINT\n", __func__);
        dmatest_exit = 1;
    }
}

void sigio_handler(int sig)
{
    if(sig == SIGIO)
    {
        sigio_received = 1;
    }
}

static void pthread_func(void *arg)
{
    int ret;
    int Oflags;
    struct f_owner_ex owner_ex;
    struct timespec time1, time2;
    struct sigaction sa, sa2;
    sigset_t set, oldset;
    long long duration;
    unsigned int mode = DMA_MEMCPY;

    ret = IFMS_MemCMAAlloc(t_para.size, &t_para.cma_block1);
    if(ret<0)
    {
        printf("CMA alloc failed.\n");
        return -1;    
    }

    ret=IFMS_MemCMAAlloc(t_para.size, &t_para.cma_block2);
    if(ret<0)
    {
        printf("CMA alloc failed.\n");
        return -1;
    }

//===========================================================================================

//Set thread name.
    prctl(PR_SET_NAME,"sigio");
    dmacopy_tid = syscall(SYS_gettid);
    printf("sigio thread tid=%ld %ld.\n", syscall(SYS_gettid), getpid());

//Set SIGIO actiong.
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sigio_handler;
    sa.sa_flags |= SA_RESTART;
    sigaction(SIGIO, &sa, NULL);

//Set proc mask.
    sigemptyset(&set);
    sigprocmask(SIG_SETMASK, &set, NULL);
    sigaddset(&set, SIGIO);

    fd = open(DMA_FILE, O_RDWR);
    if (fd < 0)
    {
        printf("Can't open %s!\n", DMA_FILE);
    }
 
    //If set F_SETOWN_EX, SIGIO will send to this thread only.
    owner_ex.pid = syscall(SYS_gettid);
    owner_ex.type = F_OWNER_TID;
    fcntl(fd, F_SETOWN_EX, &owner_ex);
    Oflags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, Oflags | FASYNC);

    clock_gettime(CLOCK_REALTIME, &time1);

    ret = ioctl(fd, DPEYE1000_AXIDMA_REQUEST_CHNN, &mode);
    if(ret < 0)
    {
        printf("fail to ioctl DPEYE1000_AXIDMA_REQUEST_CHNN!!!\n");
    }

    while(!dmatest_exit)
    {
        if(t_para.expries > 1)
            usleep(t_para.expries);
        trigger_dma_copy();     //Send DMA copy request.        while(!sigio_received)
        while(!sigio_received)
        {

            pthread_sigmask(SIG_BLOCK, &set, &oldset);
            //sigprocmask(SIG_BLOCK, &set, &oldset);
            sigsuspend(&oldset);    //Will pause here, and will be waked up by SIGIO.
            pthread_sigmask(SIG_UNBLOCK, &set, NULL);
            //sigprocmask(SIG_UNBLOCK, &set, NULL);
            handle_count++;
        }
        sigio_received = 0;
    }
    clock_gettime(CLOCK_REALTIME, &time2);
    duration = (long long)(time2.tv_sec-time1.tv_sec)*1000000000 + (time2.tv_nsec-time1.tv_nsec);
    sleep(1);
    printf("End time %lld.%09lld count=%d fps=%lld.\n", duration/1000000000, duration%1000000000, handle_count, (long long)handle_count*1000000000/duration);

    ioctl(fd, DPEYE1000_AXIDMA_RELEASE_CHNN, NULL);
    if(ret < 0)
    {
        printf("fail to ioctl DPEYE1000_AXIDMA_RELEASE_CHNN!!!\n");
    }
    close(fd);

//===========================================================================================

    ret=IFMS_MemCMAFree(t_para.cma_block1);
    if(ret<0)
    {
        printf("CMA free failed.\n");
        return -1;    
    }

    ret=IFMS_MemCMAFree(t_para.cma_block2);
    if(ret<0)
    {
        printf("CMA free failed.\n");
        return -1;    
    }
    pthread_exit(0);
}

void main(int argc, char **argv)
{
    pthread_t tidp;
    sigset_t set;
    unsigned int size = 0, expries = 0;
    struct sigaction sa;

    if(argc != 3)
    {
        printf("Usage: %s size(B) expries(us).\n", argv[0]);
        return -1;
    }
    size = atoi(argv[1]);
    if(!size)
    {
        printf("Please input right size.\n");
        return -1;
    }

    expries = atoi(argv[2]);
    if(!expries)
    {
        printf("Please input right expries time.\n");
        return -1;
    }
    t_para.size = size;
    t_para.expries = expries;

    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sigint_handler;
    sa.sa_flags |= SA_RESTART;
    sigaction(SIGINT, &sa, NULL);

    sigemptyset(&set);
    sigaddset(&set, SIGIO);
    sigprocmask(SIG_BLOCK, &set, NULL);

    if(pthread_create(&tidp, NULL, pthread_func, NULL) == -1)
    {
        printf("Create pthread error.\n");
        return;
    }
    
    if(pthread_join(tidp, NULL))
    {
        printf("Join pthread error.\n");
        return;
    }
    printf("Main exit.\n");

    return;
}
複製代碼

3.1 DMA異步和同步在不同場景下對比測試

dma_thread測試不同分辨率、不同幀率下的性能對比:

Case 同步DMA 異步DMA
top -d 5

perf record -a -e cpu-clock -- sleep 60

top -d 5 perf record -a -e cpu-clock -- sleep 60

1080p 50fps

dma_thread 3110400 20000

7.5%

單次耗時:1.6ms

91.12% swapper [kernel.kallsyms] [k] __sched_text_end
6.29% main [kernel.kallsyms] [k] dma_cookie_status
1.70% main [kernel.kallsyms] [k] dma_sync_wait
0.05% swapper [kernel.kallsyms] [k] tick_nohz_idle_enter
0.04% perf [kernel.kallsyms] [k] skip_ftrace
0.04% perf [kernel.kallsyms] [k] raw_copy_from_user
 

91.22% swapper

8.04% main 
0.43% perf 
0.11% jbd2/mmcblk1p2-
0.08% mmcqd/1

dma_sigio 3110400 20000

0.3%

單次耗時:135us


98.57% swapper 
0.53% perf 
0.46% sleep 
0.21% mmcqd/1 
0.15% jbd2/mmcblk1p2-
0.04% kworker/u2:2 
0.03% sigio

1080p 100fps

dma_thread 3110400 10000

 14.1%

單次耗時:1.6ms

84.27% swapper [kernel.kallsyms] [k] __sched_text_end
11.78% main [kernel.kallsyms] [k] dma_cookie_status
3.13% main [kernel.kallsyms] [k] dma_sync_wait
0.04% perf [kernel.kallsyms] [k] skip_ftrace
0.03% perf [kernel.kallsyms] [k] raw_copy_from_user

84.30% swapper 
14.98% main 
0.45% perf 
0.10% jbd2/mmcblk1p2-
0.09% mmcqd/1

dma_sigio 3110400 10000

0.5%

單次耗時:135us

 

98.48% swapper 
0.63% perf 
0.50% sleep 
0.21% mmcqd/1 
0.11% jbd2/mmcblk1p2-
0.05% kworker/u2:1 
0.02% sigio

4k 25fps

dma_thread 13271040 40000

 14.5%

單次耗時:6.7ms

84.71% swapper [kernel.kallsyms] [k] __sched_text_end
11.35% main [kernel.kallsyms] [k] dma_cookie_status
2.88% main [kernel.kallsyms] [k] dma_sync_wait
0.04% perf [kernel.kallsyms] [k] skip_ftrace
 

84.75% swapper 
14.53% main 
0.43% perf 
0.10% jbd2/mmcblk1p2-
0.07% mmcqd/1

dma_sigio 13271040 40000

0.2%

單次耗時:135us

 

98.51% swapper 
0.56% perf 
0.48% sleep 
0.23% mmcqd/1 
0.15% jbd2/mmcblk1p2-
0.04% kworker/u2:0 
0.03% sigio

分析總結:

1. 同步模式下,CPU佔用率跟數據量大小強相關,基本成正比;影響CPU佔用率的最大因素是DMA傳輸同步等待,即上面dma_sync_wait()和dma_cookie_status()兩個函數。

2. 異步模式下,請求發送後,交出CPU,在收到信號後繼續下一次發送,期間不會佔用CPU。CPU佔用率跟DMA請求次數強相關,主要是發送請求,以及sigsuspend()和SIGIO信號處理佔用。

3. 幀的吞吐率受DMA傳輸的幀大小影響。

3.2 那麼異步模式的極限在哪裏?

明顯DMA的異步極限幀率,同樣受限於DMA傳輸效率,並不會增大吞吐率。

那麼看看不同幀率下的CPU情況:

Case 異步DMA
top -d 5 perf record -a -e cpu-clock -- sleep 60

1080p 550 fps

(max)

2.6%

 

96.50% swapper 
1.93% sigio 
0.62% perf 
0.57% sleep 
0.21% mmcqd/1

1080p 375 fps  1.8

98.44% swapper 
0.56% perf 
0.51% sleep 
0.22% mmcqd/1 
0.16% jbd2/mmcblk1p2-
0.08% sigio

 1080p 273 fps 1.2% 

98.43% swapper 
0.58% perf 
0.51% sleep 
0.24% mmcqd/1 
0.14% jbd2/mmcblk1p2-
0.05% sigio

 4K 145 fps

(max)

0.8%  

98.45% swapper 
0.55% perf 
0.45% sleep 
0.23% mmcqd/1 
0.14% sigio

所以DMA極限幀率,主要受DMA傳輸大小和傳輸速度影響。

3.3 kernelshark對比DMA同步/異步模式

分別看看上面3個場景下同步模式下,kernelshark輸出可以看出1080p執行時間是1.67ms,4k時間是6.88ms;每次時間間隔跟fps設置也對應。

1080p 50fps、1080p 100fps、4k 25fps三種佔用率應該是1.67/21.67=7.7%、1.67/11.67=14.3%、6.88/46.88=14.7%。

再來看一下異步情況下的輸出,這時候越是大尺寸DMA傳輸CPU佔用率的收益越大。4k的時候

下面以1080p和4k對比看一下異步的收益。

1080p的DMA傳輸佔用時間從1.65,降到了1.65-1.57=0.08,收益率95%。

 

可以看出4k情況下異步DMA的CPU佔用時間從6.70,降到6.70-6.64=0.06,收益率達到99%。

4. 同進程多SIGIO衝突解決

 當測試通過,進入方案的時候遇到SIGIO無法接收到的問題。

檢查得知,原來是存在多個設備kill_fasync()。而一個進程範圍內,SIGIO只能有一個handler。

通過fcntl()設置F_SETSIG可以定義sig代提SIGIO發送信號。

操作如下:

複製代碼
#define SIGDMA (SIGRTMIN+1)--------------------------定義一個實時信號

    fcntl(fd, F_SETSIG, SIGDMA);---------------------使用SIGDMA代提SIGIO作爲async信號。

    memset(&sa, 0, sizeof(sa));
    sa.sa_sigaction = sigdma_handler;----------------一定要修改爲sa_sigaction,對應的sigdma_handler參數也需要修改。
    sa.sa_flags = SA_RESTART | SA_SIGINFO;-----------一定要增加SA_SIGINFO。
    sigaction(SIGDMA, &sa, NULL);
複製代碼

除了有上面的好處之外,實時信號還能排隊,這就比非實時信號更不會丟失。除非隊列溢出。

5. 實際場景提升效果

在實際場景中,每40ms來一幀數據進行DMA搬運,

那麼這段時間內,整個線程佔用多少時間呢?2.303-0.225=2.078ms,對應的CPU佔用率應該是5.2%。

再看看異步DMA實際效果如何?可以看出copy線程,中間調度出去的時間增大不小。

那麼此時CPU佔用率多少呢?2.288-1.177-0.431=0.68ms,對應的CPU佔用率應該是1.7%。

 

從計算來看CPU佔用率能降低3%左右。 

6. 其他方案

1. 使用AXI DMA兩通道,能否提高DMA吞吐率?相當於DMA併發,copy雙線程?------------硬件雙通道,如何構造同時觸發的雙通道case?

2. 如何標識每一次DMA傳輸,通過netlink port端口?--------------------------------------------------修改異步觸發方式,port和channel綁定

聯繫方式:[email protected]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章