1. 程式人生 > >給大家分享一篇 ACM線上測評系統評測程式設計與python實現

給大家分享一篇 ACM線上測評系統評測程式設計與python實現

寫此文目的:

  • 讓外行人瞭解ACM,重視ACM。
  • 讓ACMer瞭解評測程式評測原理以便更好得做題。
  • 讓pythoner瞭解如何使用更好的使用python。
  • 在講解之前,先給外行人補充一些關於ACM的知識。

什麼是ACM?

我們平常指的ACM是ACM/ICPC(國際大學生程式設計競賽),這是由ACM(Association for Computing Machinery,美國計算機協會)組織的年度性競賽,始於1970年,是全球大學生計算機程式能力競賽活動中最有影響的一項賽事。被譽為計算機界奧林匹克。

瞭解更多關於ACM的資訊可以參考:

什麼是ACM測評系統?

為了讓同學們擁有一個練習和比賽的環境,需要一套系統來提供服務。

系統要提供如下功能:

  • 使用者管理
  • 題目管理
  • 比賽管理
  • 評測程式

典型的ACM評測系統有兩種

評測程式是做什麼的?

評測程式就是對使用者提交的程式碼進行編譯,然後執行,將執行結果和OJ後臺正確的測試資料進行比較,如果答案和後臺資料完全相同就是AC(Accept),也就是你的程式是正確的。否則返回錯誤資訊,稍後會詳細講解。

ACM線上測評系統整體架構

為了做到低耦合,我們以資料庫為中心,前臺頁面從資料庫獲取題目、比賽列表在瀏覽器上顯示,使用者通過瀏覽器提交的程式碼直接儲存到資料庫。

評測程式負責從資料庫中取出使用者剛剛提交的程式碼,儲存到檔案,然後編譯,執行,評判,最後將評判結果寫回資料庫。

評測程式架構

評測程式要不斷掃描資料庫,一旦出現沒有評判的題目要立即進行評判。為了減少頻繁讀寫資料庫造成的記憶體和CPU以及硬碟開銷,可以每隔0.5秒掃描一次。為了提高評測速度,可以開啟幾個程序或執行緒共同評測。由於多執行緒/程序會競爭資源,對於掃描出來的一個題目,如果多個評測程序同時去評測,可能會造成死鎖,為了防止這種現象,可以使用了生產者-消費者模式,也就是建立一個待評測題目的任務佇列,這個佇列的生產者作用就是掃描資料庫,將資料庫中沒有評測的題目列表增加到任務佇列裡面。消費者作用就是從佇列中取出要評測的資料進行評測。

為什麼任務佇列能防止出現資源競爭和死鎖現象?

python裡面有個模組叫Queue,我們可以使用這個模組建立三種類型的佇列:

  • FIFO:先進先出佇列
  • LIFO:後進先出佇列
  • 優先佇列

這裡我們用到的是先進先出佇列,也就是先被新增到佇列的程式碼先被評測,保持比賽的公平性。

佇列可以設定大小,預設是無限大。

生產者發現數據庫中存在沒有評測的題目之後,使用put()方法將任務新增到佇列中。這時候如果佇列設定大小並且已經滿了的話,就不能再往裡面放了,這時候生產者就進入了等待狀態,直到可以繼續往裡面放任務為止。在等待狀態的之後生產者執行緒已經被阻塞了,也就是說不再去掃描資料庫,因此適當設定佇列的大小可以減少對資料庫的讀寫次數。

消費者需要從任務佇列獲取任務,使用get()方法,一旦某個執行緒從佇列get得到某個任務之後,其他執行緒就不能再次得到這個任務,這樣可以防止多個評測執行緒同時評測同一個程式而造成死鎖。如果任務佇列為空的話,get()方法不能獲得任務,這時候評執行緒序就會阻塞,等待任務的到來。在被阻塞的時候評測程式可以被看做停止運行了,可以明顯減少系統資源消耗。

佇列還有兩個方法:

一個是task_done(),這個方法是用來標記佇列中的某個任務已經處理完畢。

另一個是join()方法,join方法會阻塞程式直到所有的專案被刪除和處理為止,也就是呼叫task_done()方法。

這兩個方法有什麼作用呢?因為評測也需要時間,一個任務從佇列中取出來了,並不意味著這個任務被處理完了。如果沒有處理完,程式碼的狀態還是未評判,那麼生產者會再次將這個程式碼從資料庫取出加到任務佇列裡面,這將造成程式碼重複評測,浪費系統資源,影響評測速度。這時候我們需要合理用這兩個方法,保證每個程式碼都被評測並且寫回資料庫之後才開始下一輪的掃描。後面有程式碼示例。

我們使用如下程式碼建立一個FIFO佇列:

#初始化佇列
q = Queue(config.queue_size)

如何有效得從資料庫獲取資料?

這裡我們以mysql為例進行說明。python有資料庫相關的模組,使用起來很方便。這裡我們需要考慮異常處理。

有可能出現的問題是資料庫重啟了或者偶爾斷開了不能正常連線,這時候就需要不斷嘗試重新連線直到連線成功。然後判斷引數,如果是字串就說明是sql語句,直接執行,如果是列表則依次執行所有的語句,如果執行期間出現錯誤,則關閉連線,返回錯誤資訊。否則返回sql語句執行結果。

下面這個函式專門來處理資料庫相關操作

def run_sql(sql):
    '''執行sql語句,並返回結果'''
    con = None
    while True:
        try:
            con = MySQLdb.connect(config.db_host,config.db_user,config.db_password,
                                  config.db_name,charset=config.db_charset)
            break
        except: 
            logging.error('Cannot connect to database,trying again')
            time.sleep(1)
    cur = con.cursor()
    try:
        if type(sql) == types.StringType:
            cur.execute(sql)
        elif type(sql) == types.ListType:
            for i in sql:
                cur.execute(i)
    except MySQLdb.OperationalError,e:
        logging.error(e)
        cur.close()
        con.close()
        return False
    con.commit()
    data = cur.fetchall()
    cur.close()
    con.close()
    return data

需要注意的是這裡我們每次執行sql語句都要重新連線資料庫,能否一次連線,多次操作資料庫?答案是肯定的。但是,這裡我們需要考慮的問題是如何將資料庫的連線共享?可以設定一個全域性變數。但是如果資料庫的連線突然斷開了,在多執行緒程式裡面,問題就比較麻煩了,你需要在每個程式裡面去判斷是否連線成功,失敗的話還要重新連線,多執行緒情況下如何控制重新連線?這些問題如果在每個sql語句執行的時候都去檢查的話太麻煩了。

有一種方法可以實現一次連線,多次操作資料庫,還能方便的進行資料庫重連,那就是使用yield生成器,連線成功之後,通過yield將sql語句傳遞進去,執行結果通過yield反饋回來。這樣聽起來很好,但是有個問題容易被忽略,那就是yield在不支援多執行緒,多個執行緒同時向yield傳送資料,yield接收誰?yield返回一個數據,誰去接收?這樣yield就會報錯,然後停止執行。當然可以使用特殊方法對yield進行加鎖,保證每次都只有一個執行緒傳送資料。

通過測試發現,使用yield並不能提高評測效率,而每次連線資料庫也並不慢,畢竟現在伺服器效能都很高。所以使用上面的每次連線資料庫的方法還是比較好的。

還有一個問題,當多執行緒同時對資料庫進行操作的時候,也容易出現一些莫名其妙的錯誤,最好是對資料庫操作加鎖:

#建立資料庫鎖,保證一個時間只能一個程式都寫資料庫
dblock = threading.Lock()
# 讀寫資料庫之前加鎖
dblock.acquire()
# 執行資料庫操作
runsql()
# 執行完畢解鎖
dblock.release()

生產者如何去實現?

為了隱藏伺服器資訊,保證伺服器安全,所有的SQL語句都用五個#代替。

生產者就是一個while死迴圈,不斷掃描資料庫,掃描到之後就向任務佇列新增任務。

def put_task_into_queue():
    '''迴圈掃描資料庫,將任務新增到佇列'''
    while True:
        q.join() #阻塞安程式,直到佇列裡面的任務全部完成
        sql = "#####"
        data = run_sql(sql)
        for i in data:
            solution_id,problem_id,user_id,contest_id,pro_lang = i
            task = {
                "solution_id":solution_id,
                "problem_id":problem_id,
                "contest_id":contest_id,
                "user_id":user_id,
                "pro_lang":pro_lang,
            }
            q.put(task)
        time.sleep(0.5) #每次掃面完後等待0.5秒,減少CPU佔有率

消費者如何實現?

基本是按照上面說的來的,先獲取任務,然後處理任務,最後標記任務處理完成。

def worker():
    '''工作執行緒,迴圈掃描佇列,獲得評判任務並執行'''
    while True:
        #獲取任務,如果佇列為空則阻塞
        task = q.get()  
        #獲取題目資訊
        solution_id = task['solution_id']
        problem_id = task['problem_id']
        language = task['pro_lang']
        user_id = task['user_id']
        # 評測
        result=run(problem_id,solution_id,language,data_count,user_id)
        #將結果寫入資料庫
        dblock.acquire()
        update_result(result) 
        dblock.release()
        #標記一個任務完成
        q.task_done()   
如何啟動多個評測執行緒?

def start_work_thread():
    '''開啟工作執行緒'''
    for i in range(config.count_thread):
        t = threading.Thread(target=worker)
        t.deamon = True
        t.start()

這裡要注意t.deamon=True,這句的作用是當主執行緒退出的時候,評測執行緒也一塊退出,不在後臺繼續執行。

消費者獲取任務後需要做什麼處理?

因為程式碼儲存在資料庫,所以首先要將程式碼從資料庫取出來,按檔案型別命名後儲存到相應的評判目錄下。然後在評判目錄下對程式碼進行編譯,如果編譯錯誤則將錯誤資訊儲存到資料庫,返回編譯錯誤。編譯通過則執行程式,檢測程式執行時間和記憶體,評判程式執行結果。

如何編譯程式碼?

根據不同的程式語言,選擇不同的編譯器。我的評測程式支援多種程式語言。編譯實際上就是呼叫外部編譯器對程式碼進行編譯,我們需要獲取編譯資訊,如果編譯錯誤,需要將錯誤資訊儲存到資料庫。

呼叫外部程式可以使用python的subprocess模組,這個模組非常強大,比os.system()什麼的牛逼多了。裡面有個Popen方法,執行外部程式。設定shell=True我們就能以shell方式去執行命令。可以使用cwd指定工作目錄,獲取程式的外部輸出可以使用管道PIPE,呼叫communicate()方法可以可以獲取外部程式的輸出資訊,也就是編譯錯誤資訊。

可以根據編譯程式的返回值來判斷編譯是否成功,一般來說,返回值為0表示編譯成功。

有些語言,比如ruby和perl是解釋型語言,不提供編譯選項,因此在這裡僅僅加上-c引數做簡單的程式碼檢查。

python,lua,java等可以編譯成二進位制檔案然後解釋執行。

ACMer們著重看一下gcc和g++和pascal的編譯引數,以後寫程式可以以這個引數進行編譯,只要在本地編譯通過一般在伺服器上編譯就不會出現編譯錯誤問題。

可能有些朋友會有疑問:為什麼加這麼多語言?正式ACM比賽只讓用C,C++和JAVA語言啊!對這個問題,我只想說,做為一個線上測評系統,不能僅僅侷限在ACM上。如果能讓初學者用這個平臺來練習程式語言不是也很好?做ACM是有趣的,用一門新的語言去做ACM題目也是有趣的,快樂的去學習一門語言不是學得很快?我承認,有好多語言不太適合做ACM,因為ACM對時間和記憶體要求比較嚴格,好多解釋執行的語言可能佔記憶體比較大,執行速度比較慢,只要抱著一種學習程式語言的心態去刷題就好了。此外,對於新興的go語言,我認為是非常適合用來做ACM的。牛逼的haskell語言也值得一學,描述高階資料結果也很方便。感興趣的可以試試。

我的評測程式是可以擴充套件的,如果想再加其他程式語言,只要知道編譯引數,知道如何執行,配置好編譯器和執行時環境,在評測程式裡面加上就能編譯和評測。

def compile(solution_id,language):
    '''將程式編譯成可執行檔案'''
    build_cmd = {
        "gcc"    : "gcc main.c -o main -Wall -lm -O2 -std=c99 --static -DONLINE_JUDGE",
        "g++"    : "g++ main.cpp -O2 -Wall -lm --static -DONLINE_JUDGE -o main",
        "java"   : "javac Main.java",
        "ruby"   : "ruby -c main.rb",
        "perl"   : "perl -c main.pl",
        "pascal" : 'fpc main.pas -O2 -Co -Ct -Ci',
        "go"     : '/opt/golang/bin/go build -ldflags "-s -w"  main.go',
        "lua"    : 'luac -o main main.lua',
        "python2": 'python2 -m py_compile main.py',
        "python3": 'python3 -m py_compile main.py',
        "haskell": "ghc -o main main.hs",
    }
    p = subprocess.Popen(build_cmd[language],shell=True,cwd=dir_work,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
    out,err =  p.communicate()#獲取編譯錯誤資訊
    if p.returncode == 0: #返回值為0,編譯成功
        return True
    dblock.acquire()
    update_compile_info(solution_id,err+out) #編譯失敗,更新題目的編譯錯誤資訊
    dblock.release()
    return False

使用者程式碼在執行過程中是如何進行評判的(ACMer必看)?

前面說了,如果出現編譯錯誤(Compile Error),是不會執行的。每個題目都有一個標準的時間和記憶體限制,例如時間1000ms,記憶體65536K,程式在執行的時候會實時檢查其花費時間和使用記憶體資訊,如果出現超時和超記憶體將會分別返回Time Limit Exceeded和Memory Limit Exceeded錯誤資訊,如果程式執行時出現錯誤,比如非法指標,陣列越界等,將會返回Runtime Error資訊。如果你的程式沒有出現上面的資訊,說明程式順利執行結束了。接下來,就是對你的程式的輸出也就是執行結果進行檢查,如果你的執行結果和我們的標準答案完全一樣,則返回Accepted,也就說明你這個題目做對了。如果除去空格,換行,tab外完全相同,則說明你的程式碼格式錯誤,將返回Presentation Error,如果你輸出的內容有一部分和標準答案完全一樣,但是還輸出了一些其他內容,則說明你多輸出了,這時候將返回Output Limit Exceeded錯誤資訊,出現其他情況,就說明你的輸出結果和標準答案不一樣,就是Wrong Answer了。

總結一下錯誤的出現順序:

Compile Error -> Memory Limit Exceeded = Time Limit Exceeded = Runtime Error -> Wrong Answer -> Output Limit Exceeded ->Presentation Error -> Accepted

直接說難免有些空洞,做了張流程圖:
這裡寫圖片描述
如果你得到了其他資訊,比如System error,則說明伺服器端可能出問題了,我們技術人員會想法解決。如果看到waiting,說明等待評測的程式碼比較多,你需要稍作等待,直到程式碼被評測。如果你得到了Judging結果,說明你的程式碼正在評測,如果長時間一直是Judging,則說明評測程式在評測過程中可能出問題了,沒有評判出結果就停止了。技術人員會為你重判的。

希望ACMer們能根據上面的評測流程,在看到自己的評判結果的時候,能夠分析出你離AC還有多遠,以及如何改進你的程式碼才能AC。

評判答案的那部分原始碼:

def judge_result(problem_id,solution_id,data_num):
    '''對輸出資料進行評測'''
    currect_result = os.path.join(config.data_dir,str(problem_id),'data%s.out'%data_num)
    user_result = os.path.join(config.work_dir,str(solution_id),'out%s.txt'%data_num)
    try:
        curr = file(currect_result).read().replace('\r','').rstrip()#刪除\r,刪除行末的空格和換行
        user = file(user_result).read().replace('\r','').rstrip()
    except:
        return False
    if curr == user:       #完全相同:AC
        return "Accepted"
    if curr.split() == user.split(): #除去空格,tab,換行相同:PE
        return "Presentation Error"
    if curr in user:  #輸出多了
        return "Output limit"
    return "Wrong Answer"  #其他WA

注意一下,程式碼中有個replace(‘\r’,”)方法,它的作用就是將\r替換成空字串。為什麼要做這個替換呢?因為在windows下,文字的換行是”\r\n”,而在Linux下是”\n”。因為不能確定測試資料來源與windows還是Linux,增加一個\r,就是增加一個字元,如果不刪除的話,兩個文字就是不一樣的,就會造成wrong answer結果。或許你曾經遇到過在windows下用記事本開啟一個純文字檔案,格式全亂了,所有文字都在一行內,非常影響閱讀。你可以通過用寫字板開啟來解決這個問題。據說”\r\n”來源於比較古老的印表機,每列印完一行,都要先“回車(\r)”,再“換行”(\n)。同樣一個C語言的printf(“\n”)函式,在windows下將生成”\r\n”,而在Linux下生成”\n”,因為評測程式為你自動處理了,因此你就不必關注這些細節的東西了。

評測程式是如何檢測你的程式的執行時間和記憶體的?

這個問題困擾了我好久,也查了好多資料。

使用者的程式要在伺服器上執行,首先不能讓使用者的程式無限申請記憶體,否則容易造成宕機現象,需要將程式的記憶體限制在題目規定的最大記憶體內。其次要限制使用者程式的執行時間,不能讓使用者的程式無限制執行。

一般解決方案是:在使用者的程式執行前,先做好資源限制,限制程式能使用的最大記憶體和CPU佔用,當用戶的程式一旦超出限制就自動終止了。還有個比較重要的問題是如何獲取程式執行期間的最大記憶體佔用率。使用者的程式碼在執行前需要申請記憶體,執行期間還能動態申請和釋放記憶體,執行完畢釋放記憶體。程式執行時還有可能使用指標等底層操作,這無疑給檢測記憶體造成更大的困難。在windows下,程式執行結束後,可以呼叫系統函式獲取程式執行期間的最大記憶體,貌似在Linux下沒用現成的函式可以呼叫。

在Linux下,我們可以使用ps或top命令來獲取或監視在某個時刻應用程式的記憶體佔用率,要獲取程式的最大執行記憶體,就要不斷去檢測,不斷去比較,直到程式結束,獲取最大值就是使用者程式執行期間的最大記憶體。根據這個設想,我寫了一個程式來實現這個想法:

def get_max_mem(pid):
    '''獲取程序號為pid的程式的最大記憶體'''
    glan = psutil.Process(pid)
    max = 0
    while True:
        try:
            rss,vms = glan.get_memory_info()
            if rss > max:
                max = rss
        except:
            print "max rss = %s"%max
            return max

def run(problem_id,solution_id,language,data_count,user_id):
    '''獲取程式執行時間和記憶體'''
    time_limit = (time_limit+10)/1000.0
    mem_limit = mem_limit * 1024
    max_rss = 0
    max_vms = 0
    total_time = 0
    for i in range(data_count):
        '''依次測試各組測試資料'''
        args = shlex.split(cmd)
        p = subprocess.Popen(args,env={"PATH":"/nonexistent"},cwd=work_dir,stdout=output_data,stdin=input_data,stderr=run_err_data)
        start = time.time()
        pid = p.pid
        glan = psutil.Process(pid)
        while True:
            time_to_now = time.time()-start + total_time
            if psutil.pid_exists(pid) is False:
                program_info['take_time'] = time_to_now*1000
                program_info['take_memory'] = max_rss/1024.0
                program_info['result'] = result_code["Runtime Error"]
                return program_info
            rss,vms = glan.get_memory_info()
            if p.poll() == 0:
                end = time.time()
                break
            if max_rss < rss:
                max_rss = rss
                print 'max_rss=%s'%max_rss
            if max_vms < vms:
                max_vms = vms
            if time_to_now > time_limit:
                program_info['take_time'] = time_to_now*1000
                program_info['take_memory'] = max_rss/1024.0
                program_info['result'] = result_code["Time Limit Exceeded"]
                glan.terminate()
                return program_info
            if max_rss > mem_limit:
                program_info['take_time'] = time_to_now*1000
                program_info['take_memory'] = max_rss/1024.0
                program_info['result'] =result_code["Memory Limit Exceeded"]
                glan.terminate()
                return program_info

        logging.debug("max_rss = %s"%max_rss)
#        print "max_rss=",max_rss
        logging.debug("max_vms = %s"%max_vms)
#        logging.debug("take time = %s"%(end - start))
    program_info['take_time'] = total_time*1000
    program_info['take_memory'] = max_rss/1024.0
    program_info['result'] = result_code[program_info['result']]
    return program_info

上面的程式用到了一些程序控制的一些知識,簡單說明一下。

程式的基本原理是:先用多程序庫subprocess的Popen函式去建立一個新的程序,獲取其程序號(pid),然後用主執行緒去監測這個程序,主要是監測實時的記憶體資訊。通過比較函式,獲得程式的執行期間的最大記憶體。什麼時候停止呢?有四種情況:

程式執行完正常結束。這個我們可以通過 subprocess.Popen裡面的poll方法來檢測,如果為0,則代表程式正常結束。
程式執行時間超過了規定的最大執行時間,用terminate方法強制程式終止
程式執行記憶體超過了規定的最大記憶體,terminate強制終止。
程式執行期間出現錯誤,異常退出了,這時候我們通過檢查這個pid的時候就會發現不存在。
還有一點是值得注意的:上文提到在編譯程式的時候,呼叫subprocess.Popen,是通過shell方式呼叫的,但是這裡沒有使用這種方式,為什麼呢?這兩種方式有什麼區別?最大的區別就是返回的程序的pid,以shell方式執行,返回的pid並不是子程序的真正pid,而是shell的pid,當我們去檢查這個pid的記憶體使用率的時候得到的並不是使用者程序的pid!不通過shell方式去呼叫外部程式則是直接返回真正程式的pid,而不用去呼叫shell。官方文件是這麼說的:if shell is true, the specified command will be executed through the shell.

如果不用shell方式去執行命令的話,傳遞引數的時候就不能直接將字串傳遞過去,例如ls -l這個命令ls和引數-l,當shell=False時,需要將命令和引數變成一個列表[‘ls’,’-l’]傳遞過去。當引數比較複雜的時候,將命令分隔成列表就比較麻煩,幸好python為我們提供了shlex模組,裡面的split方法就是專門用來做這個的,官方文件是這麼說的:Split the string s using shell-like syntax.,最好不要自己去轉換,有可能會導致錯誤而不能執行。

上面的檢測記憶體和時間的方法靠譜嗎?

不靠譜,相當不靠譜!(當然學學python如何對程序控制也沒壞處哈!)為什麼呢?有點經驗的都知道,C語言的執行效率比python高啊!執行速度比python快!這會造成什麼後果?一個簡單的hello world小程式,C語言“瞬間”就執行完了,還沒等我的python程式開始檢測就執行完了,我的評測程式什麼都沒檢測到,然後返回0,再小的程式記憶體也不可能是0啊!在OJ上顯示記憶體為0相當不科學!

那怎麼辦?能不能讓C語言的程式執行速度慢下來?CPU的頻率是固定的,我們沒法專門是一個程式的佔用的CPU頻率降低,在windows下倒是有變速齒輪這款軟體可以讓軟體執行速度變慢,不知道在Linux下有沒有。還有沒有其他辦法?聰明的你也許會想到gdb除錯,我也曾經想用這種方法,用gdb除錯可以使程式單步執行,然後程式執行一步,我檢測一次,多好,多完美!研究了好一陣子gdb,發現並不是那麼簡單。首先,我們以前用gdb除錯C/C++的時候,在編譯的時候要加上一個-g引數,然後執行的時候可以單步執行,此外,還有設定斷點什麼的。有幾個問題:

其他語言如何除錯?比如java,解釋執行的,直接除錯java虛擬機器嗎?
如何通過python對gdb進行控制?還有獲取執行狀態等資訊。
這些問題都不是很好解決。

那上面的方法測量的時間準嗎?不準!為什麼?我們說的程式的執行時間,嚴格來說是佔用CPU的時間。因為CPU採用的是輪轉時間片機制,在某個時刻,CPU在忙別的程式。上面的方法用程式執行的結束時間減去開始時間,得到的時間一定比它實際執行的時間要大。如果程式執行速度過快,不到1毫秒,評測程式也不能檢測出來,直接返回0了。

如何解決時間和記憶體的測量問題?

後來在v2ex上發了一個帖子提問,得到高人指點,使用lorun。lorun是github上的一個開源專案,專案地址:https://github.com/lodevil/Lo-runner,這是用C語言寫的一個python擴充套件模組,讓程式在一個類似沙盒的環境下執行,然後精準的獲取程式的執行時間和記憶體,還能對程式進行限制,限制程式的系統呼叫。原文是這麼說的:We use this python-c library to run program in a sandbox-like environment. With it, we can accurately known the resource using of the program and limit its resource using including system-call interrupt.。安裝使用都非常方便。我主要用它來測量執行時間和記憶體,後期程式碼檢查還是用我的程式。

感興趣的同學可以將這個模組下載下來,作為本地測試使用,可以預先生成一些測試資料,然後測量你的程式碼的執行時間和記憶體,比對你的答案是否正確。

不同程式語言時間記憶體如何限定?

一般來說,假設C/C++語言的標程是時間限制:1000ms,記憶體限制32768K,那麼java的時間和記憶體限制都是標準限制的2倍,即2000ms,65536K。

由於後來我再OJ增加了好多其他語言,我是這樣規定的:編譯型的語言和速度較快的解釋型語言的時間和記憶體限制和C/C++是一樣的,這樣的語言包括:C、C++、go、haskell、lua、pascal,其他速度稍慢的解釋執行的語言和JAVA是一樣的,包括:java、python2、python3、ruby、perl。畢竟使用除C,C++,JAVA外的語言的朋友畢竟是少數,如果限制太嚴格的話可以根據實際情況對其他程式語言放寬限制。

多組測試資料的題目時間和記憶體如何測算?

多組測試資料是一組一組依次執行,時間和記憶體取各組的最大值,一旦某組測試資料時間和記憶體超出限制,則終止程式碼執行,返回超時或超記憶體錯誤資訊。

如何防止惡意程式碼破壞系統?

我們可以使用以下技術來對使用者程式進行限制:

lorun模組本身就有限制,防止外部呼叫
降低程式的執行許可權。在Linux下,目錄許可權一般為755,也就是說,如果換成一個別的使用者,只要不是所有者,就沒有修改和刪除的許可權。python裡面可以使用os.setuid(int(os.popen(“id -u %s”%”nobody”).read()))來將程式以nobody使用者的身份執行
設定沙盒環境,將使用者執行環境和外部隔離。Linux下的chroot命令可以實現,python也有相關方法,但是需要提前搭建沙盒環境。用jailkit可以快速構建沙盒環境,感興趣的朋友可以看看
使用ACL訪問控制列表進行詳細控制,讓nobody使用者只有對某個資料夾的讀寫許可權,其他資料夾禁止訪問
評判機和伺服器分離,找單獨的機器,只負責評判
對使用者提交的程式碼預先檢查,發現惡意程式碼直接返回Runtime Error
禁止評測伺服器連線外網,或者通過防火牆限制網路訪問
如何啟動和停止評測程式以及如何記錄錯誤日誌?

啟動很簡單,只要用python執行protect.py就行了。

如果需要後臺執行的話可以使用Linux下的nohup命令。

為了防止同時開啟多個評測程式,需要將以前開啟的評測程式關閉。

為了方便啟動,我寫了這樣一個啟動指令碼:

#!/bin/bash
sudo kill `ps aux | egrep "^nobody .*? protect.py" | cut -d " "  -f4`
sudo nohup python protect.py &

第一條命令就是殺死多餘的評測程序,第二條是啟動評測程式。

在程式裡面使用了logging模組,是專門用來記錄日誌的,這麼模組很好用,也很強大,可定製性很強,對我們分析程式執行狀態有很大幫助。下面是一些示例:

2013-03-07 18:19:04,855 --- 321880 result 1
2013-03-07 18:19:04,857 --- judging 321882
2013-03-07 18:19:04,881 --- judging 321883
2013-03-07 18:19:04,899 --- judging 321884
2013-03-07 18:19:04,924 --- 321867 result 1
2013-03-07 18:19:04,950 --- 321883 result 7
2013-03-07 18:19:04,973 --- 321881 result 1
2013-03-07 18:19:05,007 --- 321884 result 1
2013-03-07 18:19:05,012 --- 321882 result 4
2013-03-07 18:19:05,148 --- judging 321885
2013-03-07 18:19:05,267 --- judging 321886
2013-03-07 18:19:05,297 --- judging 321887
2013-03-07 18:19:05,356 --- judging 321888
2013-03-07 18:19:05,386 --- judging 321889
2013-03-07 18:19:05,485 --- 321885 result 1

python的配置檔案如何編寫?

最簡單有效的方式就是建立一個config.py檔案,裡面寫上配置的內容,就像下面一樣:

#!/usr/bin/env python
#coding=utf-8
#開啟評測執行緒數目
count_thread = 4
#評測程式佇列容量
queue_size = 4
#資料庫地址
db_host = "localhost"
#資料庫使用者名稱
db_user = "user"
#資料庫密碼
db_password = "password"
#資料庫名字
db_name = "db_name"

使用的時候只需要將這個檔案匯入,然後直接config.queue_size就可以訪問配置檔案裡面的內容,很方便的。

評測程式的評測效率如何?

自從伺服器啟用新的評測程式之後,已經經歷了兩次大的比賽和幾次大型考試,在幾百個人的比賽和考試中,評測基本沒用等待現象,使用者提交的程式碼基本都能立即評測出來。大體測了一下,單伺服器平均每秒能判6個題目左右(包括獲取程式碼,編譯,執行,檢測,資料庫寫入結果等流程)。評測程式目前已經穩定運行了幾個月,沒有出現大的問題,應該說技術比較成熟了。

評測程式還能繼續改進嗎?

當時腦子估計是被驢踢了,居然使用多執行緒來評測!有經驗的python程式猿都知道,python有個全域性GIL鎖,這個鎖會將python的多個執行緒序列化,在一個時刻只允許一個執行緒執行,無論你的機器有多少個CPU,只能使用一個!這就明顯影響評測速度!如果換成多程序方式,一個評測程序佔用一個CPU核心,評測速度將會是幾倍幾十倍的效能提升!到時候弄個上千人的比賽估計問題也不大,最起碼評測速度能保證。

此外,還可以構建一個分散式的評測伺服器叢集,大體設想了一下可以這樣實現:

首先,可以選一臺伺服器A專門和資料庫互動,包括從資料庫中獲取評測任務以及評測結束將結果寫回資料庫。然後選擇N臺普通計算機作為評測機,評測機只和資料庫A打交道,也就是從伺服器A獲取任務,在普通機器上評測,評測完後將結果反饋到伺服器A,再由A將結果寫入到資料庫。伺服器A在這裡就充當一個任務管理和分配的角色,協調各個評測機去評測。這樣可以減少對資料庫的操作,評測機就不用去一遍一遍掃資料庫了。評測的速度和安全性可以得到進一步提升。
這裡寫圖片描述

 
整理不易,如果覺得有所幫助,希望可以留下您的精彩言論再走。趕快為你們最喜歡的框架打Call吧。

大家如果需要Python的學習資料可以加我的Qun:834179111,小編整理了,從Python入門零基礎到專案

實戰的資料。歡迎還沒有找到方向的小夥伴來學習。
 
本文轉自網路 如有侵權 請聯絡小編刪除

相關推薦

大家分享 ACM線上測評系統評測程式設計python實現

寫此文目的: 讓外行人瞭解ACM,重視ACM。 讓ACMer瞭解評測程式評測原理以便更好得做題。 讓pythoner瞭解如何使用更好的使用python。 在講解之前,先給外行人補充一些關於ACM的知識。 什麼是ACM? 我們平常指的ACM是ACM/IC

今天大家分享關於幾個C語言幾個難題

字符 多少 std stdio.h c語言 程序 是什麽 include 輸出 .下面這個程序的輸出結果是什麽? #include<stdio.h>int main(){int i=43;printf("%d\n",printf("

今天大家分享關於幾個C語言幾個難題!

.下面這個程式的輸出結果是什麼? #include<stdio.h> int main() { int i=43; printf("%d\n",printf("%d",printf("%d",i))); return 0; } 參考答案:本程式將輸出4321。原因在於先輸出i

大家分享Python抓取漫畫並製作mobi格式電子書

  想看某一部漫畫,但是用手機看感覺螢幕太小,用電腦看吧有太不方面。正好有一部Kindle,決定寫一個爬蟲把漫畫爬取下來,然後製作成 mobi 格式的電子書放到kindle裡面看。 一、編寫爬蟲程式   用Chrome瀏覽器開啟目標網站,按下F12 啟動“開

大家分享 python +splinter自動重新整理搶票

一年一度的春運又來了, 今年我自己寫了個搶票指令碼。 python +splinter自動重新整理搶票,可以成功搶到(依賴自己的網路環境太厲害,還有機器的好壞), 但是感覺不是很完美, 有大神請指導完善一下(或者有沒有別的好點的思路) 不勝感謝

大家分享 python有趣的解包用法

python中的解包可以這樣理解:一個list是一個整體,想把list中每個元素當成一個個個體剝離出來,這個過程就是解包,我們來看下面這些例子(分為12個部分)。 1.將list中每個元素賦值給一個變數 >>> name, age, d

大家分享 http上傳協議之檔案流實現,輕鬆支援大檔案上傳

最近在公司進行業務開發時遇到了一些問題,當需要上傳一個較大的檔案時,經常會遇到記憶體被大量佔用的情況。公司之前使用的web框架是一個老前輩實現的。在實現multipart/form-data型別的post請求解析時, 是將post請求體一次性讀到記憶體中再做解析

大家分享 如何拿到半數面試公司Offer——我的Python求職之路

從八月底開始找工作,短短的一星期多一些,面試了9家公司,拿到5份Offer,可能是因為我所面試的公司都是些創業性的公司吧,不過還是感觸良多,因為學習Python的時間還很短,沒想到還算比較容易的找到了工作,就把這些天的面試經驗和大家分享一下,希望為學習Pyt

大家分享 python_列表結構模擬棧和佇列

棧的儲存方式是先進後出,具有push和pop的行為。佇列的儲存方式是先進先出(FIFO) 實現主要包括連續push、pop棧頂和展示棧內元素三個方法。 連續push採用raw_input,以#作為結束標誌;展示元素以倒序依次展示的方式,用到了很重要的copy模

ACM線上評測系統 各大高校的ACM線上測評系統

點選開啟連結 山東理工大學http://acm.sdut.edu.cn/ 南陽理工學院http://acm.nyist.net/JudgeOnline/ 浙江大學http://acm.zju.edu.cn  北京大學http://acm.pku.edu.cn/JudgeOn

轉載關於springmvc下session的用法,覺得作者寫的不錯,大家分享一下

繫結模型物件中某個屬性 Spring 2.0 定義了一個 org.springframework.ui.ModelMap 類,它作為通用的模型資料承載物件,傳遞資料供檢視所用。我們可以在請求處理方法中宣告一個 ModelMap 型別的入參,Spring 會將本次請求模型物

今天黃老師大家分享財商是什麼?

  財商是什麼?簡單地說:是經濟人在現實社會裡的生存能力,是一個人判斷怎樣掙錢的敏銳性,是會計、投資、市場營銷和法律等各方面能力的綜合。     一、財商是一種理財的智慧   羅伯特·清崎認為:“理財不是你賺了多少錢,而是你有多少錢,錢為你工作的努力程度,以及你的錢能維持

九個讓你驚訝的冷門知識-有點意思,分享大家

媽媽們在懷孕前過量飲酒,她們的孩子可能發生血糖升高,或者其他血糖功能上的改變。這些改變會提高這些孩子在成年以後患糖尿病的風險。 十歲的時候聽力最好,然後越往後越差。 每年都有超過兩萬只鳥撞在窗子上死亡的。 女生的性活躍區域與大腦海馬體中神經組織的生長有關

剛開發了款新的單機手機遊戲大家分享一下

技術 pro watermark shadow 喜歡 ima term 分享圖片 RoCE 剛開發的遊戲開心的杯子是一款利用物理畫線的益智遊戲,玩家通過畫線使用水填滿玻璃杯,沒有固定的攻略,全部是靠玩家的想象力。喜歡的朋友下載:https://at.umeng.com/jy

真實說說個人親身經歷北京賽車pk10真假改單,大家分享一些穩贏內幕!

技術 計劃 月份 奮鬥 股市 賽車 推薦 努力 收入 從失敗到成功,有時候僅一步之遙,有時候卻要跨過99到坎;成功需要努力,需要磨練,需要奮鬥,就算是一步之遙,一不小心也會失之千裏;我們都是股市投資的尋路者,追夢人,為成功而來,為成功而執著。讓我們同舟共濟,攜手而行,永不放

大家分享一下------mysql的優化

對數 phantom 狀態 表級鎖 部分 鎖機制 單獨 常量 行記錄 MySQL 優化專題拓展 --王耀宇 一、SQL優化 1、分析和定位策略 1、通過 s

下面我大家首 《肖邦》!!!!!!!!!!

邏輯運算 小數 lis 效果 取反 優先級 als 內部 bool 一:數據類型 1:可變數據類型 在ID不變的情況下,數據類型內部的元素改變 列表list和字典dict  2: 不可變數劇類型 value改變,ID也跟著改變

前兩天遇到了錯誤好久才整明白大家分享下:

openssl verify 展開 errno ssl cert error led detail 錯誤 錯誤:(Network error [errno 60]: SSL certificate problem, verify that the CA cert is OK

老婆的文章

mysql老婆公司有個這樣的需求: 查詢出王者榮耀的用戶回流信息,當用戶連續兩天登陸,則判定為2日回流,如果間隔一天登陸,則判定為3日回流,如果間隔5天登陸,則判定為7日回流。用戶數據間隔時間短為14天(固定)。 準備數據 database語法和mysql一致。 創建表: create table use

大家聊雲收藏從 Spring Boot 1.0 升級到 2.0 所踩的坑

springboot 雲收藏 先給大家曬一下雲收藏的幾個數據,作為一個 Spring Boot 的開源項目(https://github.com/cloudfavorites/favorites-web)目前在 Github 上面已經有1600多個 Star,如果按照 SpringBoot 標簽進行篩