golang 실행 명령을 사용하여 시간 초과 설정

목차

0x00 머리말

0x01 먼저 os / exec 실행

0x02 os / exec 블랙 매직

0x03 채널 및 고 루틴

채널

고 루틴

몇몇 문제들

0x04 시간 초과로 명령 실행


최근에는 Golang으로 명령을 실행하고 명령에 대한 특정 시간 제한을 설정해야합니다. 이를 시작으로 기본 셸 명령을 실행하는 것으로 시작하여 점차 흑 마법을 연구했습니다.

0x00 머리말

이 기사에서는 사전 지식이 없으며 자신있게 먹을 수 있다고 가정합니다.

바로 어제 인터뷰에서 Go-routine의 원리와 CSP의 개념에 대해 물었습니다. 첫 번째 diss는 매우 학문적이라고 느끼고 연구원 대신 보안 알고리즘 작업 엔지니어를 요청하는 의미를 모르는 인터뷰 학생의 물결입니다. 초보자  는 CSP 동시 사용 모드 의 정의를 확실히 알지 못합니다  ., 그리고 "Golang을 좋아한다면 깊이 공부하지 않겠습니까"라는 질문과 기타 불편한 문제들 (저는 ML을 처음 접했고 언어 기능을 공부하지 않았다고 말했었습니다) 여기에 간단한 연구가 있습니다.

그러나 기사는 여전히 이것을 제목으로 사용하고 있습니다. 처음의 요구 사항 외에도이 부분의 지식에 대한 인터뷰 팁은 작업 앞에서 개를 핥는 것입니다 (x

0x01 먼저 os / exec 실행

공식 문서의 샘플 코드를 사용하여 명령을 실행하고 출력을 얻습니다.

func RunCommand(command string, args ...string) (stdout string) {
	cmd := exec.Command(command, args...)
	outBytes, err := cmd.Output()
	if err != nil {
		log.Fatal(err)
	}
	stdout = string(outBytes)
	return
}

func main() {
	log.Print(RunCommand("echo", "fuck"))
}

출력 2018/11/21 22:27:00 fuck.

0x02 os / exec 블랙 매직

os / exev에는 약간의 흑 마법이 있습니다.

echo_loop라는 새로운 bash 스크립트를 만들었습니다.

while true; do date; sleep 1; done

을 실행했을 때 RunCommand("bash", "echo_loop.sh")프로그램이 멈췄습니다. 분명히이 스크립트는 지속적으로 출력되고 있으며, 우리는 지속적으로 중간 출력을 얻고 자합니다. 그러나 우리는 그가 갇히게 할 수 없습니다. 그렇지 않으면 시간 초과를 감지하는 방법. 명령 cmd.Run()이 완료 때까지 기다리는 명령 객체에 대한 몇 가지 작업이 있으며 명령이 기다리지 cmd.Start()않도록 할 수 있습니다.

func RunCommand(command string, args ...string) (stdout string) {
	cmd := exec.Command(command, args...)
	err := cmd.Start()
	if err != nil {
		log.Fatal(err)
	}
//	stdout = string(outBytes)
	return
}

그러나 지금 출력을 얻으려면 전달할 수 있습니다 cmd.StdoutPipe()( Start()이전에 수행해야 함을 유의하십시오 . 그렇지 않으면 출력 내용은 exec: StdoutPipe after process started). 이것을 추가 한 후에 우리는 다시 멈춰 있습니다.

func RunCommand(command string, args ...string) (stdout string) {
	cmd := exec.Command(command, args...)
	err := cmd.Start()
	if err != nil {
		log.Fatal(err)
	}
	out, err := cmd.StdoutPipe()
	if err != nil {
		log.Fatal(err)
	}
	defer out.Close()
	outBytes, err := ioutil.ReadAll(out)
	if err != nil {
		log.Fatal(err)
	}
	stdout = string(outBytes)
	return
}

물론 cmd.Wait()오류 유형을 반환하는 또 다른 하나가 있으므로 이와 같은 출력을 얻을 수 있습니다. 불행히도 그것은 또한 붙어 있습니다.

func RunCommand(command string, args ...string) (stdout string) {
	cmd := exec.Command(command, args...)
	err := cmd.Start()
	if err != nil {
		log.Fatal(err)
	}
	outBytes := cmd.Wait()
	stdout = outBytes.Error()
	return
}

우리는 자연스럽게 작업을 생각하고 포 그라운드에 있지 않은 명령을 실행하고 (타임 아웃을 제어하기 위해) 출력을 얻어야합니다. 다행히 Golang은 이러한 메커니즘을 구현합니다.

0x03 채널 및 고 루틴

먼저 코드를 살펴 보겠습니다.

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	time.Sleep(2 * time.Second)
	c <- sum // 发送数据到 Channel
}

func main() {
	s := []int{1, 2, 3, 4, 5, 6}
	c := make(chan int)
	log.Print("Waiting")
	go sum(s[len(s)/2:], c) // 前半段
	go sum(s[:len(s)/2], c) // 后半段
	x, y := <-c, <-c // 分别阻塞地接收数据
	log.Print("Done")
	log.Print(x, y)
}

// Output:
//	2018/11/21 23:51:54 Waiting
//	2018/11/21 23:51:56 Done
//	2018/11/21 23:51:56 15 6

채널

채널은 Go의 핵심 유형입니다. 동시 코어 유닛이 통신을 위해 데이터를 보내거나받을 수있는 파이프 라인으로 생각할 수 있습니다. 기본 작동 방법은 다음과 같습니다.

ch := make(chan int)	// 新建一个 Channel
ch <- v					// 发送值 v 到 Channel ch中
v := <-ch				// 从 Channel ch 中接收数据,并将数据赋值给 v

채널은 단방향 및 양방향, 트랜시버 유형 및 용량으로 설정할 수 있습니다. 채널에 대한 일부 지식은 공식 문서 또는 이 문서를 참조하십시오 . 주목할 가치가있는 한 가지 문제는 수신 작업 중에 채널에 데이터가 없으면 여기에서 프로그램이 차단된다는 것입니다.이 때 고 루틴에서  데이터를 써야합니다 . 동일한 액세스 원칙은 모두 차단되며  이를 읽으려면 고 루틴이 필요합니다.

고 루틴

Routine과 관련하여 실제로 복잡한 호출 방법이 없습니다. 함수를 호출하기 전에 식별자를 추가하기 go만하면됩니다. 그래서 우리는 종종 그것을 goroutine이라고 부릅니다. 우리가 잘 아는 스레드로 볼 수 있지만 구현이 더 가볍습니다. Golang은 스레드 풀을 구현합니다. 구체적인 구현은 나중에 [0x05 인터뷰 팁] 섹션에서 언급 될 것입니다.

고 루틴과 채널도 상호 보완 적입니다. 파이썬에는 비슷한 thread.join()개념 이 없기 때문입니다 . 앞에서 언급했듯이 채널 액세스가 차단됩니다. 기본 채널 크기는 0이며이를 unbuffered 라고합니다 . 버퍼링되지 않은 채널은 메시지를 가져오고 저장할 때 현재 고 루틴을 일시 중단합니다. 따라서 우리는 채널에 데이터를 쓰는 고 루틴을 완료 할 수 있으며, 쓰기 후에 채널은 데이터를 읽고 나머지 부분을 계속 실행할 수 있습니다.

기본 채널 크기는 0이며이를 unbuffered 라고 합니다. 우리는 크기를 기본값으로 설정하지 않고 버퍼링수 있습니다 . 버퍼링되지 않은 채널이 데이터를 전달하도록하는 것은 교착 상태의 중요한 원인 중 하나이며 채널에 크기를 지정하면이를 피할 수 있습니다.

몇몇 문제들

Q1 : 기록 된 데이터를 읽지 않으면 어떻게됩니까? 읽은 데이터가 없으면 어떻게합니까?

모든 교착 상태, 오류 메시지는 다음과 같습니다.all goroutines are asleep - deadlock

Done을 ​​인쇄하기 전에 문장을 추가하면 c <- 0교착 상태가 발생합니다. 아무도이 데이터를 읽지 않습니다.

Done을 ​​인쇄하기 전에 문장을 추가하면 _ = <- c교착 상태가 발생합니다. 읽은 데이터가 없습니다.

Q2 :이 두 문장을 동시에 추가하는 것은 어떻습니까?

또한 교착 상태입니다.

요점 : 단일 고 루틴의 작업 데이터는 교착 상태 여야합니다 . 0에서 c로 넘길 때 쓰레드 풀에 조인 된 쓰레드를 읽어야합니다. 이때 메인 쓰레드를 제외한 다른 고 루틴은 없습니다. 오류 메시지에서 말했듯이 모든 고 루틴은 수면 상태입니다.

Q3 : 어떻게 교착 상태를 유지할 수 있습니까?

좋은 질문. 순환 읽기와 순환 대기가 발생하여 버퍼링되지 않은 채널이 데이터를 전달하는 교착 상태의 원리를 알아야합니다. 다음은 일반적인 루프 대기의 예입니다.

c, quit := make(chan int), make(chan int)

go func() {
   c <- 1 // c 正在等待数据被取走,才能执行 quit
   quit <- 0 // 楼上等待期间这里也在等
}()

x := <- quit // quit 在等待 goroutine 有人写
y := <- c // 由于一直没有 quit,c 中数据不可能被取走

평신도의 관점에서 채널에 저장된 데이터는 하나씩 꺼내야합니다. 정말로 필요한 경우 이전 검색과 마찬가지로 하나씩 꺼내지 못하며 채널에 크기를 지정할 수 있습니다. 여기 c, quit := make(chan int, 1), make(chan int, 1)작성 하면 위의 프로그램은 교착 상태가되지 않습니다.

물론 수천만 개의 버그가 있으며, 우리는 교착 상태가 아니고 버그가 아닌 반례를 제공하기 위해 여기에 있습니다. 그 이유는이 고 루틴이 실행되기 전에 기본 프로그램이 종료되기 때문입니다.

c := make(chan int)

go func() {
   c <- 1
}()

 

0x04 시간 초과로 명령 실행

우리는 원래 문제를 해결 한 것 같습니다 : 명령이 포 그라운드에 있지 않을 때 (고 cmd.Wait()루틴으로 시작됨) 명령을 실행 하고 출력 (채널을 통해)을 얻어야합니다. 원래 코드를 다음과 같이 수정합니다.

  1. SigInt 및 Kill 스레드를 보낼 때 파이프 라인 사이의 시퀀스를 닫는 마법 같은 작업을 피하기 위해 (StdoutPipe는 종료 후 콘텐츠를 가져올 수 없으며 종료되지 않으면 대기를 차단합니다) 이 기사 의 처리 방법 참조하십시오 . 바이트 버퍼를 통해 수락하십시오.
  2. 정상 작동을 완료하고 Kill을 종료하는 명령의 작동을 처리하려면 select를 사용하십시오.
  3. SigInt 신호를 보낸 후 일부 명령은 일부 정보 (예 : ping)를 출력하여 Kill 전에 10ms 지연을 추가합니다.

코드는 다음과 같으며 테스트 실행을 직접 복사 할 수 있습니다.

package main

import (
	"syscall"
	"time"
	"bytes"
	"log"
	"os/exec"
)

func RunCommandWithTimeout(timeout int, command string, args ...string) (stdout, stderr string, isKilled bool) {
	var stdoutBuf, stderrBuf bytes.Buffer
	cmd := exec.Command(command, args...)
	cmd.Stdout = &stdoutBuf
	cmd.Stderr = &stderrBuf
	cmd.Start()
	done := make(chan error)
	go func() {
		done <- cmd.Wait()
	}()
	after := time.After(time.Duration(timeout) * time.Millisecond)
	select {
		case <-after:
			cmd.Process.Signal(syscall.SIGINT)
			time.Sleep(10*time.Millisecond)
			cmd.Process.Kill()
			isKilled = true
		case <-done:
			isKilled = false
	}
	stdout = string(bytes.TrimSpace(stdoutBuf.Bytes())) // Remove \n
	stderr = string(bytes.TrimSpace(stderrBuf.Bytes())) // Remove \n
	return
}

func main() {
	resultOut, resultErr, resultStat := RunCommandWithTimeout(3000, "ping", "baidu.com")
	
	log.Print("Is Killed: ", resultStat)
	log.Print("Res: \n===\n", resultOut, "\n===")
	log.Print("Err: \n===\n", resultErr, "\n===")
}

// Output:
//	2018/11/22 01:47:44 Is Killed: true
//	2018/11/22 01:47:44 Res: 
//	===
//	PING baidu.com (220.181.57.216): 56 data bytes
//	64 bytes from 220.181.57.216: icmp_seq=0 ttl=53 time=44.607 ms
//	64 bytes from 220.181.57.216: icmp_seq=1 ttl=53 time=44.476 ms
//	64 bytes from 220.181.57.216: icmp_seq=2 ttl=53 time=45.519 ms
//
//	--- baidu.com ping statistics ---
//	3 packets transmitted, 3 packets received, 0.0% packet loss
//	round-trip min/avg/max/stddev = 44.476/44.867/45.519/0.464 ms
//	===
//	2018/11/22 01:47:44 Err: 
//	===
//
//	===

 

 

추천

출처blog.csdn.net/whatday/article/details/113837586