在線代碼評測機


評測機

思路

通過父進程啟動一個子進程,子進程運行用戶提交的代碼。通過父進程監控子進程使用的內存,時間等計算機資源,最后對子進程生成的答案與正確答案進行比對。

第一步:生成一個進程

用戶提交的代碼可能會編譯不通過,這時需要將編譯報錯信息返回給用戶。我在編譯的makefile命令后加這一句make >err.txt 2>&1將編譯報錯的信息輸出到一個文件內

函數fork
一個現有的進程可以調用fork函數創建一個新進程。

#include<unistd.h>
pid_t fork(void);

子進程和父進程繼續執行fork調用之后的指令。子進程是父進程的副本。子進程獲得父進程數據空間、堆、棧的副本,父子進程並不共享。父子進程共享正文段。

例子:

#include <iostream>
#include <unistd.h>
#include <sys/types.h>        // 提供類型 pid_t 的定義
#include <sys/wait.h>
#include <sys/resource.h>

int main()
{
    pid_t pid = fork();

    if(pid < 0) {
        std::cout << "error" << std::endl;
        exit(0);
    } else if(pid == 0) {
        std::cout << "in child: now id = " << getpid() << std::endl;  //子進程id
        std::cout << "in child: father id = " << getppid() << std::endl; //父進程id
    } else if(pid > 0) {
        std::cout << "in father: child id = " << pid << std::endl;    //子進程id
        std::cout << "in father: now id = " << getpid() << std::endl; //父進程id
    }
    return 0;
}

第二歩:執行用戶的代碼

當我們調用fork函數后,生成的子進程執行的代碼與父進程一樣這不是我們想要的。
這時我們可以調用exec函數,該進程執行的程序完全替換為新程序,而新程序從其main函數開始執行。替換了當前進程的正文段、數據段、堆段和棧段。

exec
int execl(const char *pathname,const char *agr0,.../*(char*)0*/);
int execv(const char *pathname,char *cosnt agrv[]
int execle(const char *pathname,const char *agr0,.../*(char*)0,char *const envp[]*/);
int execve(const char *pathname,char *const agrv[],char *const envp[])
int execlp(const char *filename,const char *arg0,.../*(char*)0*/);
int execvp(const char *filename,char* const agrv[]
int fexecve(int fd,char *const agrv[],char *const envp[]

詳見《UNIX環境高級編程》第八章第十節

#include <bits/stdc++.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/resource.h>

//bash為相對路徑
void start_bash(std::string bash) {
    char *c_bash = new char[bash.length() + 1];     
    strcpy(c_bash, bash.c_str());
    char* const child_args[] = { c_bash, NULL };
    execv(child_args[0], child_args);    //調用exec        
    delete []c_bash;
}

int main()
{
    pid_t pid = fork();

    if(pid < 0) {
        std::cout << "error" << std::endl;
        exit(0);
    } else if(pid == 0) {
        std::cout << "in child" << getppid() << std::endl;
	start_bash(std::string("test2"));
    } else if(pid > 0) {
        std::cout << "in father" << getpid() << std::endl;
    }
    return 0;
}

第三步:限制子進程的資源使用

獲得子進程運行時間

獲取當前系統時間:std::chrono::system_clock::now();
返回的類型std::chrono::system_clock::time_point

得到運行時間的代碼
using namespace std::chrono;
system_clock::time_point begin_time,end_time;

begin_time = system_clock::now();  //獲取運行前時間

std::cout << "start"<<std::endl;

for (int i=0; i<100000000; ++i)
    int num =i;
std::cout << "end"<<std::endl;

end_time = system_clock::now();     //獲取運行后時間

duration<double> time_span = duration_cast<duration<double>>(end_time - begin_time);

std::cout << "run time = " << time_span.count() << " seconds."<< std::endl;

用戶所提交的代碼有可能會需要非常長的時間甚至根本就是死循環。當用戶的程序超出所規定時間的時候應該主動關閉進程。

下面介紹兩個函數getrlimit/setrlimit

#include<sys/resource.h>
int getrlimit(int resource,struct rlimit *rlptr)
int setrlimit(int resource,const struct rlimit *rlptr)
struct rlimit{
    rlim_t rlim_cur;  //軟限值
    rlim_t rlim_max;  //硬限值
}

在更改資源限制時,須遵循下列 3 條規則。
(1)任何一個進程都可將一個軟限制值更改為小於或等於其硬限制值。
(2)任何一個進程都可降低其硬限制值,但必須不小於其軟限制值。這種降低對普通用戶而言是不可逆的。
(3)只有超級用戶進程可以提高硬限制值。
詳見《UNIX環境高級編程》第七章第十一節

在子進程中加入以下測試的代碼:

rlimit limit;
limit.rlim_cur = limit.rlim_max = 1; //設置資源限制參數
setrlimit(RLIMIT_CPU , &limit);      //設置資源限制
int i = 0;
while(1) {
    i++;
}

可以觀察到程序在運行大約1s后被終止,但是父進程如何知道?
當一個進程正常或異常終止時,內核就向其父進程發送SIGCHLD信號。父進程可以選擇忽略它,或者調用信號處理函數處理。

pid_t wait3(int *status,int options,struct rusage *rusage);
pid_t wait4(pid_t pid,int *status,int options,struct rusage *rusage);

wait3() 和 wait4() 函數除了可以獲得子進程狀態信息外,還可以獲得子進程的資源使用信息,這些信息是通過參數 rusage 得到的。而 wait3() 與 wait4() 之間的區別是,wait3() 等待所有進程,而 wait4() 可以根據 pid 的值選擇要等待的子進程,參數 pid 的意義與 waitpid() 函數的一樣。

struct rusage的定義

struct rusage {
    struct timeval ru_utime; // user time used 
    struct timeval ru_stime; // system time used 
    long ru_maxrss; // maximum resident set size 
    long ru_ixrss; // integral shared memory size
    long ru_idrss; // integral unshared data size 
    long ru_isrss; // integral unshared stack size 
    long ru_minflt; // page reclaims 
    long ru_majflt; // page faults 
    long ru_nswap;// swaps
    long ru_inblock; // block input operations 
    long ru_oublock; // block output operations 
    long ru_msgsnd; // messages sent 
    long ru_msgrcv; //messages received 
    long ru_nsignals; // signals received 
    long ru_nvcsw; // voluntary context switches 
    long ru_nivcsw; // involuntary context switches 
};

通過wait4()得到結構體rusage可以獲得子進程運行的信息

獲得子進程運行時間
auto use_time = use.ru_utime.tv_sec*1000+use.ru_utime.tv_usec/1000 + use.ru_stime.tv_sec*1000+use.ru_stime.tv_usec/1000;

獲得進程最大使用內存

liunx關於文件描述的文件存在 /proc/進程號/status 的文件內,通過不停讀取文件內VmPeak: 內存使用量 的信息可以得到該進程的實時內存使用量,此外依舊通過setrlimit()來限制內存使用量。

static long get_proc_mem(unsigned int pid){

    char file_name[64]={0};
    FILE *fd;
    char line_buff[512]={0};
    sprintf(file_name,"/proc/%d/status",pid);
    //std::cout<<file_name<<std::endl;
    fd =fopen(file_name,"r");
    if(nullptr == fd){
        return 0;
    }

    char name[64];
    long long vmrss = 0;
    for (int i=0; i<17-1;i++){
        fgets(line_buff,sizeof(line_buff),fd);
    }

    fgets(line_buff,sizeof(line_buff),fd);
    sscanf(line_buff,"%s %d",name,&vmrss);
    fclose(fd);
    return vmrss;
}

第四步:獲取子進程結束的狀態

用戶的進程可能不會正常的結束。

當我們調用的wait4(pid_t pid,int *status,int options,struct rusage *rusage)函數,其中第二個參數status是代表進程結束的狀態。當子進程結束后會向父進程發送信號

序號 信號 作用
1 SIGHUP 本信號在用戶終端連接(正常或非正常)結束時發出, 通常是在終端的控制進程結束時, 通知同一session內的各個作業, 這時它們與控制終端不再關聯。
2 SIGINT 程序終止(interrupt)信號,在用戶鍵入INTR字符(通常是Ctrl-C)時發出,用於通知前台進程組終止進程。
3 SIGQUIT 和SIGINT類似, 但由QUIT字符(通常是Ctrl-/)來控制.進程在因收到SIGQUIT退出時會產生core文件, 在這個意義上類似於一個程序錯誤信號。
4 SIGILL 執行了非法指令. 通常是因為可執行文件本身出現錯誤, 或者試圖執行數據段.堆棧溢出時也有可能產生這個信號。
5 SIGTRAP 由斷點指令或其它trap指令產生. 由debugger使用。
6 SIGABRT 調用abort函數生成的信號。
7 SIGBUS 非法地址, 包括內存地址對齊(alignment)出錯。比如訪問一個四個字長的整數, 但其地址不是4的倍數。它與SIGSEGV的區別在於后者是由於對合法存儲地址的非法訪問觸發的(如訪問不屬於自己存儲空間或只讀存儲空間)。
8 SIGFPE 在發生致命的算術運算錯誤時發出. 不僅包括浮點運算錯誤,還包括溢出及除數為0等其它所有的算術的錯誤。
9 SIGKILL 用來立即結束程序的運行.本信號不能被阻塞、處理和忽略。如果管理員發現某個進程終止不了,可嘗試發送這個信號。
10 SIGUSR1 留給用戶使用
11 SIGSEGV 試圖訪問未分配給自己的內存, 或試圖往沒有寫權限的內存地址寫數據.
12 SIGUSR2 留給用戶使用
13 SIGPIPE 管道破裂。這個信號通常在進程間通信產生,比如采用FIFO(管道)通信的兩個進程,讀管道沒打開或者意外終止就往管道寫,寫進程會收到SIGPIPE信號。此外用Socket通信的兩個進程,寫進程在寫Socket的時候,讀進程已經終止。
14 SIGALRM 時鍾定時信號, 計算的是實際的時間或時鍾時間. alarm函數使用該信號.
15 SIGTERM 程序結束(terminate)信號,與SIGKILL不同的是該信號可以被阻塞和處理。通常用來要求程序自己正常退出,shell命令kill缺省產生這個信號。如果進程終止不了,我們才會嘗試SIGKILL。
16 SIGCHLD 子進程結束時, 父進程會收到這個信號。如果父進程沒有處理這個信號,也沒有等待(wait)子進程,子進程雖然終止,但是還會在內核進程表中占有表項,這時的子進程稱為僵屍進程。這種情況我們應該避免(父進程或者忽略S
18 SIGCONT 讓一個停止(stopped)的進程繼續執行. 本信號不能被阻塞.可以用一個handler來讓程序在由stopped狀態變為繼續執行時完成特定的工作. 例如, 重新顯示提示符
19 SIGSTOP 停止(stopped)進程的執行. 注意它和terminate以及interrupt的區別:該進程還未結束, 只是暫停執行. 本信號不能被阻塞, 處理或忽略.
20 SIGTSTP 停止進程的運行, 但該信號可以被處理和忽略.用戶鍵入SUSP字符時(通常是Ctrl-Z)發出這個信號
21 SIGTTIN 當后台作業要從用戶終端讀數據時, 該作業中的所有進程會收到SIGTTIN信號.缺省時這些進程會停止執行.
22 SIGTTOU 類似於SIGTTIN, 但在寫終端(或修改終端模式)時收到.
23 SIGURG 有"緊急"數據或out-of-band數據到達socket時產生.
24 SIGXCPU 超過CPU時間資源限制. 這個限制可以由getrlimit/setrlimit來讀取/改變。
25 SIGXFSZ 當進程企圖擴大文件以至於超過文件大小資源限制。
26 SIGVTALRM 虛擬時鍾信號. 類似於SIGALRM, 但是計算的是該進程占用的CPU時間.
27 SIGPROF 類似於SIGALRM/SIGVTALRM, 但包括該進程用的CPU時間以及系統調用的時間.
28 SIGWINCH 窗口大小改變時發出.
29 SIGIO 文件描述符准備就緒, 可以開始進行輸入/輸出操作.
30 SIGPWR Power failure
31 SIGSYS 非法的系統調用。

下面是我用來判斷各種狀態的代碼

if(WIFSIGNALED(status)){

    printf("child killed by %d\nstatus = %d\n", WTERMSIG(status),status);
    switch (WTERMSIG(status)){
        case SIGXCPU:
        case SIGKILL:                 //超出時間限制
            std::cout<<"TLE"<<std::endl;
            result.result = JudgeResult ::TLE;
            break;
        case SIGXFSZ:                 //輸出超出限制
            std::cout<<"OLE"<<std::endl;
            result.result = JudgeResult ::OLE;
            break;
        case SIGSEGV:                 //全局數組超過限制
        case SIGABRT:                 //申請堆內存失敗
            result.result = JudgeResult ::MLE;
            std::cout<<"MLE"<<std::endl;
        break;
        default:{                     //運行錯誤
            result.result = JudgeResult ::RE;
            std::cout<<"RE"<<std::endl;
        }
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM