簡體   English   中英

如何從Linux C程序讀取計數器到bash測試腳本?

[英]How to read a counter from a linux C program to a bash test script?

我在Suse linux系統上有一個大型C / C ++程序。 我們使用bash腳本對其進行自動化測試,該腳本將輸入發送到程序,並讀取輸出。 它主要是“黑匣子”測試,但是某些測試需要了解一些內部細節才能確定測試是否通過。

特別是一項測試,需要知道程序運行特定功能(解析特定響應消息)的時間。 該函數運行時,它將發出日志並增加計數器變量。 當前,自動化測試通過在日志文件的日志文件中grep並確定測試前后的發生次數來確定調用次數。 這是不理想的,因為不能保證日志(syslog-ng),並且經常被配置關閉,因為它們基本上是調試日志。

我正在尋找更好的選擇。 我可以更改程序以增強可測試性,但是對正常運行不會造成重大影響。 我的第一個想法是,每次測試后我都可以閱讀計數器。 像這樣:

gdb --pid=$PID --batch -ex "p numServerResponseX"

它在運行時速度很慢,但是很好,因為根本不需要更改程序。 稍作工作,我可能可以編寫一個ptrace命令來更有效地執行此操作。

但是我想知道是否沒有更簡單的方法可以做到這一點。 我可以將計數器寫入共享內存(使用shm_open / mmap),然后在bash腳本中讀取/ dev / shm嗎? 有沒有一些更簡單的方法可以設置計數器以使其易於讀取,而又不會使其緩慢增加?

編輯:

詳細信息:測試設置如下:

testScript <-> sipp <-> programUnderTest <-> externalServer

bash testScript使用sipp注入sip消息,並且通常根據sipp中的完成代碼確定成功或失敗。 但是在某些測試中,它需要知道程序從外部服務器收到的響應數。 函數“ processServerResponseX”處理來自外部服務器的某些響應。 在測試過程中,沒有太多流量在運行,因此在10秒內,該函數可能僅被調用20次。 當每個測試結束並且我們要檢查計數器時,基本上應該沒有流量。 但是,在正常操作期間,它可能每秒被調用數百次。 該函數大致為:

unsigned long int numServerResponseX;
int processServerResponseX(DMsg_t * dMsg, AppId id)
{
   if (DEBUG_ENABLED)
   {
       syslog(priority, "%s received %d", __func__, (int) id);
   }
   myMutex->getLock();
   numServerResponseX++;
   doLockedStuff(dMsg, id);
   myMutex->releaseLock();

   return doOtherStuff(dMsg, id);
}

該腳本當前執行以下操作:

grep processServerResponseX /var/log/logfile | wc -l

並比較之前和之后的值。 我的目標是即使DEBUG_ENABLED為false也要進行這項工作,並且不要太慢。 該程序是多線程的,並且在i86_64 smp計算機上運行,​​因此添加任何長時阻止功能將不是一個好的解決方案。

我將使用某個函數“(解析特定的響應消息)”在某處寫入(可能使用fopen然后fprintf然后fclose )一些文本數據。

該目標可以是FIFO(請參閱fifo(7) ...)或tmpfs文件系統(它是RAM文件系統)中的臨時文件,可能是/run/

如果您的C ++程序足夠大且復雜,則可以考慮添加一些探測工具(外部程序可以查詢C ++程序的內部狀態的某種方式),例如專用的Web服務(在單獨的線程中使用libonion ),或者一些與systemdD-bus的接口 ,或一些遠程過程調用服務,例如ONC / RPCJSON-RPC等。

您可能對POCOlib感興趣。 也許其日志記錄框架應該使您感興趣。

如前所述,您可以使用Posix共享內存和信號量(請參見shm_overview(7)sem_overview(7) ...)。

也許您需要Linux特定的eventfd(2) 。...(您可以編寫一個小型C程序,以供測試bash腳本調用...。)

您也可以嘗試更改命令行(我忘了怎么做,也許是libproc或寫到/proc/self/cmdlineproc(5) ...)。 然后ps將顯示它。

我個人通常會使用Basile Starynkevitch概述的方法,但是我想提出一種使用實時信號的替代方法。

我並不是說這是最好的解決方案,但它易於實現且開銷很小。 主要缺點是請求和響應的大小都限制為一個 int (或者從技術上講,任何可以由intvoid *表示的東西)。

基本上,您使用一個簡單的幫助程序將信號發送到應用程序。 該信號的有效負載為您的應用程序可以檢查的一個int ,並基於此有效負載,應用程序通過將相同的信號發送回始發方作為響應,並以其自身的int作為有效負載。

如果不需要任何鎖定,則可以使用簡單的實時信號處理程序。 當捕獲到信號時,它將檢查siginfo_t結構。 如果通過sigqueue()發送,則該請求位於siginfo_t結構的si_value成員中。 處理程序使用sigqueue()響應來響應原始過程(結構的si_pid成員)。 這僅需要將大約六十行代碼添加到您的應用程序。 這是一個示例應用程序app1.c

#define  _POSIX_C_SOURCE 200112L
#include <unistd.h>
#include <signal.h>
#include <errno.h>

#include <string.h>
#include <time.h>
#include <stdio.h>

#define   INFO_SIGNAL (SIGRTMAX-1)

/* This is the counter we're interested in */    
static int counter = 0;

static void responder(int signum, siginfo_t *info,
                      void *context __attribute__((unused)))
{
    if (info && info->si_code == SI_QUEUE) {
        union sigval value;
        int response, saved_errno;

        /* We need to save errno, to avoid interfering with
         * the interrupted thread. */
        saved_errno = errno;

        /* Incoming signal value (int) determines
         * what we respond back with. */
        switch (info->si_value.sival_int) {

        case 0: /* Request loop counter */
            response = *(volatile int *)&counter;
            break;

        /* Other codes? */

        default: /* Respond with -1. */
            response = -1;
        }

        /* Respond back to signaler. */
        value.sival_ptr = (void *)0L;
        value.sival_int = response;
        sigqueue(info->si_pid, signum, value);

        /* Restore errno. This way the interrupted thread
         * will not notice any change in errno. */
        errno = saved_errno;
    }
}

static int install_responder(const int signum)
{
    struct sigaction act;
    sigemptyset(&act.sa_mask);
    act.sa_sigaction = responder;
    act.sa_flags = SA_SIGINFO;
    if (sigaction(signum, &act, NULL))
        return errno;
    else
        return 0;
}

int main(void)
{
    if (install_responder(INFO_SIGNAL)) {
        fprintf(stderr, "Cannot install responder signal handler: %s.\n",
                        strerror(errno));
        return 1;
    }
    fprintf(stderr, "PID = %d\n", (int)getpid());
    fflush(stderr);

    /* The application follows.
     * This one just loops at 100 Hz, printing a dot
     * about once per second or so. */

    while (1) {
        struct timespec t;

        counter++;

        if (!(counter % 100)) {
            putchar('.');
            fflush(stdout);
        }

        t.tv_sec = 0;
        t.tv_nsec = 10000000; /* 10ms */
        nanosleep(&t, NULL);

        /* Note: Since we ignore the remainder
         *       from the nanosleep call, we
         *       may sleep much shorter periods
         *       when a signal is delivered. */
    }

    return 0;
}

上面的響應者使用counter值響應查詢0 ,而其他響應則返回-1 您只需在responder()添加適當的case語句即可添加其他查詢。

請注意,鎖定原語( sem_post()除外)不是異步信號安全的 ,因此不應在信號處理程序中使用。 因此,以上代碼無法實現任何鎖定。

信號傳遞可以中斷阻塞調用中的線程。 在上面的應用程序中, nanosleep()調用通常被信號傳遞中斷,從而導致睡眠被縮短。 (類似地,如果read()write()調用被信號傳遞中斷,則它們的返回值可能為errno == EINTR ,返回-1 。)

如果這是一個問題,或者不確定所有代碼是否正確處理errno == EINTR ,或者您的計數器需要鎖定,則可以改用專用於信號處理的單獨線程。

除非傳遞信號,否則專用線程將進入睡眠狀態,並且只需要很小的堆棧,因此在運行時它實際上不會消耗任何大量資源。

目標信號在所有線程中被阻塞,專用線程在sigwaitinfo()等待。 如果它捕獲到任何信號,則按上面的方式處理它們-區別在於,由於這本身是線程而不是信號處理程序,因此您可以自由地使用任何鎖定等,而不必將自己限制在異步信號安全范圍內職能。

這種線程化方法稍長一些,將近一百行代碼添加到您的應用程序中。 (差異包含在app1.c responder()install_responder()函數中;甚至添加到main()的代碼也與app1.c中的app1.c 。)

這是app2.c

#define  _POSIX_C_SOURCE 200112L
#include <signal.h>
#include <errno.h>
#include <pthread.h>

#include <string.h>
#include <time.h>
#include <stdio.h>

#define   INFO_SIGNAL (SIGRTMAX-1)

/* This is the counter we're interested in */    
static int counter = 0;

static void *responder(void *payload)
{
    const int signum = (long)payload;
    union sigval response;
    sigset_t sigset;
    siginfo_t info;
    int result;

    /* We wait on only one signal. */
    sigemptyset(&sigset);
    if (sigaddset(&sigset, signum))
        return NULL;

    /* Wait forever. This thread is automatically killed, when the
     * main thread exits. */
    while (1) {

        result = sigwaitinfo(&sigset, &info);
        if (result != signum) {
            if (result != -1 || errno != EINTR)
                return NULL;
            /* A signal was delivered using *this* thread. */
            continue;
        }

        /* We only respond to sigqueue()'d signals. */
        if (info.si_code != SI_QUEUE)
            continue;

        /* Clear response. We don't leak stack data! */
        memset(&response, 0, sizeof response);

        /* Question? */
        switch (info.si_value.sival_int) {

        case 0: /* Counter */
            response.sival_int = *(volatile int *)(&counter);
            break;

        default: /* Unknown; respond with -1. */
            response.sival_int = -1;
        }

        /* Respond. */
        sigqueue(info.si_pid, signum, response);
    }
}

static int install_responder(const int signum)
{
    pthread_t worker_id;
    pthread_attr_t attrs;
    sigset_t mask;
    int retval;

    /* Mask contains only signum. */
    sigemptyset(&mask);
    if (sigaddset(&mask, signum))
        return errno;

    /* Block signum, in all threads. */
    if (sigprocmask(SIG_BLOCK, &mask, NULL))
        return errno;

    /* Start responder() thread with a small stack. */
    pthread_attr_init(&attrs);
    pthread_attr_setstacksize(&attrs, 32768);
    retval = pthread_create(&worker_id, &attrs, responder,
                            (void *)(long)signum);
    pthread_attr_destroy(&attrs);    

    return errno = retval;
}

int main(void)
{
    if (install_responder(INFO_SIGNAL)) {
        fprintf(stderr, "Cannot install responder signal handler: %s.\n",
                        strerror(errno));
        return 1;
    }
    fprintf(stderr, "PID = %d\n", (int)getpid());
    fflush(stderr);

    while (1) {
        struct timespec t;

        counter++;

        if (!(counter % 100)) {
            putchar('.');
            fflush(stdout);
        }

        t.tv_sec = 0;
        t.tv_nsec = 10000000; /* 10ms */
        nanosleep(&t, NULL);
    }

    return 0;
}

對於app1.capp2.c ,應用程序本身是相同的。 對應用程序的唯一修改是確保所有必需的頭文件都獲得#include d,添加responder()install_responder() ,並盡早在main()調用install_responder() main()

app1.capp2.c僅在app2.c responder()install_responder()有所不同;並且app2.c需要pthreads。)

app1.capp2.c使用信號SIGRTMAX-1 ,在大多數應用程序中均app1.c使用該信號。

app2.c方法也app2.c具有有用的副作用:如果您在應用程序中使用其他信號,但又不希望它們中斷阻止I / O調用等,也許您有由第三方編寫的庫,不能正確處理EINTR ,但是您確實需要在應用程序中使用信號-您可以在應用程序中的install_responder()調用之后簡單地阻止信號。 這樣,唯一沒有阻塞信號的線程就是響應者線程,內核將使用tat傳遞信號。 因此,唯一會被信號傳遞中斷的線程是響應者線程,更具體地說是sigwaitinfo()中的sigwaitinfo() responder() ,它忽略任何中斷。 如果您使用例如異步I / O或計時器,或者這是一個繁重的數學或數據處理應用程序,那么這可能會很有用。

可以使用非常簡單的查詢程序query.c來查詢這兩個應用程序的實現:

#define _POSIX_C_SOURCE 200112L
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <stdio.h>

int query(const pid_t process, const int signum,
          const int question, int *const response)
{
    sigset_t prevmask, waitset;
    struct timespec timeout;
    union sigval value;
    siginfo_t info;
    int result;

    /* Value sent to the target process. */
    value.sival_int = question;

    /* Waitset contains only signum. */
    sigemptyset(&waitset);
    if (sigaddset(&waitset, signum))
        return errno = EINVAL;

    /* Block signum; save old mask into prevmask. */
    if (sigprocmask(SIG_BLOCK, &waitset, &prevmask))
        return errno;

    /* Send the signal. */
    if (sigqueue(process, signum, value)) {
        const int saved_errno = errno;
        sigprocmask(signum, &prevmask, NULL);
        return errno = saved_errno;
    }

    while (1) {

        /* Wait for a response within five seconds. */
        timeout.tv_sec = 5;
        timeout.tv_nsec = 0L;

        /* Set si_code to an uninteresting value,
         * just to be safe. */
        info.si_code = SI_KERNEL;

        result = sigtimedwait(&waitset, &info, &timeout);
        if (result == -1) {
            /* Some other signal delivered? */
            if (errno == EINTR)
                continue;
            /* No response; fail. */
            sigprocmask(SIG_SETMASK, &prevmask, NULL);
            return errno = ETIMEDOUT;
        }

        /* Was this an interesting signal? */
        if (result == signum && info.si_code == SI_QUEUE) {
            if (response)
                *response = info.si_value.sival_int;
            /* Return success. */
            sigprocmask(SIG_SETMASK, &prevmask, NULL);
            return errno = 0;
        }
    }
}

int main(int argc, char *argv[])
{
    pid_t pid;
    int signum, question, response;
    long value;
    char dummy;

    if (argc < 3 || argc > 4 ||
        !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s PID SIGNAL [ QUERY ]\n", argv[0]);
        fprintf(stderr, "\n");
        return 1;
    }

    if (sscanf(argv[1], " %ld %c", &value, &dummy) != 1) {
        fprintf(stderr, "%s: Invalid process ID.\n", argv[1]);
        return 1;
    }
    pid = (pid_t)value;
    if (pid < (pid_t)1 || value != (long)pid) {
        fprintf(stderr, "%s: Invalid process ID.\n", argv[1]);
        return 1;
    }

    if (sscanf(argv[2], "SIGRTMIN %ld %c", &value, &dummy) == 1)
        signum = SIGRTMIN + (int)value;
    else
    if (sscanf(argv[2], "SIGRTMAX %ld %c", &value, &dummy) == 1)
        signum = SIGRTMAX + (int)value;
    else
    if (sscanf(argv[2], " %ld %c", &value, &dummy) == 1)
        signum = value;
    else {
        fprintf(stderr, "%s: Unknown signal.\n", argv[2]);
        return 1;
    }
    if (signum < SIGRTMIN || signum > SIGRTMAX) {
        fprintf(stderr, "%s: Not a realtime signal.\n", argv[2]);
        return 1;
    }

    /* Clear the query union. */
    if (argc > 3) {
        if (sscanf(argv[3], " %d %c", &question, &dummy) != 1) {
            fprintf(stderr, "%s: Invalid query.\n", argv[3]);
            return 1;
        }
    } else
        question = 0;

    if (query(pid, signum, question, &response)) {
        switch (errno) {
        case EINVAL:
            fprintf(stderr, "%s: Invalid signal.\n", argv[2]);
            return 1;
        case EPERM:
            fprintf(stderr, "Signaling that process was not permitted.\n");
            return 1;
        case ESRCH:
            fprintf(stderr, "No such process.\n");
            return 1;
        case ETIMEDOUT:
            fprintf(stderr, "No response.\n");
            return 1;
        default:
            fprintf(stderr, "Failed: %s.\n", strerror(errno));
            return 1;
        }
    }

    printf("%d\n", response);

    return 0;
}

注意,這里我沒有對信號編號進行硬編碼。 app1.capp2.c的命令行上使用SIGRTMAX-1 (您可以更改它query.c的確也理解SIGRTMIN+n 。您必須使用實時信號SIGRTMIN+0SIGRTMAX-0 (含)。)

您可以使用以下命令編譯所有三個程序

gcc -Wall -O3 app1.c -o app1
gcc -Wall -O3 app2.c -lpthread -o app2
gcc -Wall -O3 query.c -o query

./app1./app2打印其PID,因此您無需查找它。 (不過,您可以使用ps -o pid= -C app1ps -o pid= -C app2找到PID。)

如果在一個外殼程序(或兩個外殼程序中)中運行./app1./app2 ,則可以看到它們每秒大約輸出一次點。 計數器每1/100秒增加一次。 (按Ctrl + C停止。)

如果在同一台計算機上同一目錄中的另一個Shell中運行./query PID SIGRTMAX-1 ,則可以看到計數器值。

在我的計算機上運行的示例:

A$ ./app1
PID = 28519
...........

B$ ./query 28519 SIGRTMAX-1
11387

C$ ./app2
PID = 28522
...

B$ ./query 28522 SIGRTMAX -1
371

如前所述,此機制的缺點是響應僅限於一個int (或從技術上講是intvoid * )。 但是,也可以通過使用Basile Starynkevich概述的某些方法來解決此問題。 通常,該信號只是針對應用程序的通知,它應更新存儲在文件,共享內存段或任何地方的狀態。 我建議為此使用專用線程方法,因為它的開銷很小,並且對應用程序本身的影響也很小。

任何問題?

硬編碼的systemtap解決方案可能如下所示:

% cat FOO.stp
global counts
probe process("/path/to/your/binary").function("CertainFunction") { counts[pid()] <<< 1 }
probe process("/path/to/your/binary").end { println ("pid %d count %sd", pid(), @count(counts[pid()]))
                                            delete counts[pid()] }

# stap FOO.stp
pid 42323 count 112
pid 2123 count 0
... etc, until interrupted

感謝您的答復。 其他答案中有很多很好的信息。 但是,這就是我所做的。 首先,我調整了程序以在shm文件中添加計數器:

struct StatsCounter {
    char counterName[8];
    unsigned long int counter;
};
StatsCounter * stats;

void initStatsCounter()
{
    int fd = shm_open("TestStats", O_RDWR|O_CREAT, 0);

    if (fd == -1)
    {
        syslog(priority, "%s:: Initialization Failed", __func__);
        stats = (StatsCounter *) malloc(sizeof(StatsCounter));
    }
    else
    {
        // For now, just one StatsCounter is used, but it could become an array.
        ftruncate(fd, sizeof(StatsCounter));
        stats = (StatsCounter *) mmap(NULL, sizeof(StatsCounter), 
            PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    }

    // Initialize names. Pad them to 7 chars (save room for \0).
    snprintf(stats[0].counterName, sizeof(stats[0].counterName), "nRespX ");
    stats[0].counter = 0;
}

然后將processServerResponseX更改為在鎖定部分增加stats [0] .counter。 然后,我更改了腳本以使用“ hexdump”解析shm文件:

hexdump /dev/shm/TestStats -e ' 1/8 "%s " 1/8 "%d\n"'

然后將顯示如下內容:

nRespX  23

這樣,如果我還想查看響應Y,則可以在以后進行擴展...

如果在更改文件時訪問文件,則不確定hexdump是否存在互斥問題。 但就我而言,我認為這並不重要,因為腳本僅在測試之前和之后調用它,所以它不應在更新過程中。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM