리눅스 쉘 스크립트 중 백그라운드로 프로세스 실행할때 궁금한점이 있습니다.

morolty의 이미지

쉘 스크립트를 짜던 도중, 백그라운드로 java 프로그램을 실행 시킨 후,

바로 다음줄에 ps -ef | grep 을 이용하여 pid를 가져오는 스크립트를 작성하였습니다.

java -jar \$JAR_FILE_NAME.jar > /dev/null 2>&1 &
ps -ef | grep \$JAR_FILE_NAME | grep -v grep | awk '{print \$2}' > \$PID_FILE

그런데 테스트환경에서는 아무런 문제 없이 잘 되다가 실제로 배포하고 나니 첫 실행때 pid를 못가져 오는 버그가 발생하였습니다.

어찌저찌 해서 백그라운드로 실행 시킨 후 너무 빠르게 pid를 검색했나 싶어서 sleep 1을 주니 해결이 되어 그대로 배포했습니다.

이 과정에서 질문사항이 두가지 생겼는데요,

1. 리눅스 시스템에서 예를들어 find / -name asdf 를 했을 경우 첫번째는 매우 느리다가 두번째부턴 빠르게 찾아지는 경험을 했습니다.
이 경우는 인덱싱 작업 같은걸 해서 빨라지는건가 하고 추측하고 있었는데, 프로세스 실행 시에도 이와 비슷한 현상으로
첫 실행때와 두번째 실행때 실행되는 속도가 다른건지요? 만약 맞다면 혹시 이유를 알 수 있을까요??

2. 대학교때 배웠던 기억으로는, c언어 컴파일 속도는 cpu 클럭에 전적으로 의존하고 있다고 기억하고 있습니다. (싱글코어 프로그래밍)
쉘 스크립트의 경우도 동일한가요? 그리고 프로세스 올라가는 속도도 cpu에 의존하는것인가요?

긴 글 읽어주셔서 감사합니다.

질문이 약간 두서없을지도 모르겠습니다만 리플달아주시면 정말 감사드리겠습니다.

chanik의 이미지

질문의 요지와는 동떨어진 얘기입니다만,
가장 최근에 실행한 백그라운드 프로세스의 아이디는 $! 으로 알아낼 수 있습니다.
위의 두 번째 줄은 아래와 같이 쓸 수 있죠.

java -jar \$JAR_FILE_NAME.jar > /dev/null 2>&1 &
echo $! > \$PID_FILE
morolty의 이미지

정말 감사합니다! 며칠동안 ps -ef 쓰면서 아무리 생각해도 뭔가 있을것 같은데 못알아내서 결국 시간에 쫓기어 그대로 작성했었는데 여기서 해결책을 얻네요! 감사합니다 ^^

ymir의 이미지

1. directory, file 에 대한 접근도 캐시 메모리에 저장됩니다.
두번째 find 에서는 캐시에서 읽어오니 처음보다 빠르겠죠.
linux memory buffers cached, drop caches 로 검색해 보세요.

되면 한다! / feel no sorrow, feel no pain, feel no hurt, there's nothing gained.. only love will then remain.. 『 Mizz 』

morolty의 이미지

반드시 한번 읽어보도록 하겠습니다, 감사합니다

tweedledum의 이미지

이미 문제를 해결하셨지만. 재밌는 문제네요.

1. background process 의 PID를 ps 명령으로 가져올 때
처음 명령으로 PID를 가져올때도 있고 그렇지 않을 때도 있다면 race condition 이 원인일 것 같은데요. 백그라운드 작업을 실행시키고 다음에 ps 명령으로 PID를 가져오도록 쉘에서 순서를 개런티해주는 방법을 모르겠군요. 궁금합니다. 알고 계시는 분이 있는지.

(command_a ; command_b) & 또는
(command_a && command_b) & 이런 방법으로는 안될 것 같고요.

2. 쉘 내장 환경 변수를 쓸 때
쉘 내장 환경변수를 쓰는 방법은 ps 명령을 쓰는 것보다 더 안전해 보입니다. 아무래도 fork() 리턴값을 가져와서 출력해줄 것이기 때문에 커널에서 어떤 일이 벌어지던 간에 순서가 지켜질 수 밖에 없을 것으로 생각됩니다.

3. 그런데 애초에 왜 이런 문제가 발생하는지...

command_a -> background job
command_b -> ps / echo

fork(); // first fork
                                        fork(); // parent of first fork, second fork
                                        exec(command_b); // child of second fork
exec(command_a); // child of first fork

위와 같은 순서로 실행된다면 아마도 ps 가 실패할 가능성이 농후해 보입니다.

하지만 내장 환경 변수를 사용한다면....

위의 순서로 실행된다고 하더라도 이미 first fork 때 pid 를 환경 변수로 저장하고 있다가 second fork 때 그 환경변수가 그대로 넘어가겠지요.

그래서 내장 환경 변수를 사용할 때(echo $!)는 항상 올바른 PID 를 출력해줄 것 같습니다.

tweedledum의 이미지

새로운 프로그램을 실행시킬 때 fork() 와 exec() 이 분리되기 때문에 발생하는 race condition이라고 할 수 있을 것 같네요.

이 문제를 재현해보려고 한다면 간단한 C 프로그램을 만들어서... 첫번째 fork() 실행후에 child 에서 sleep을 몇 초 줘보면 재현해볼 수 있을 것 같습니다.

morolty의 이미지

race condition 문제일 경우 랜덤하게 버그가 나타났을 테지만, 두번째 실행 이후부터는 제대로 동작 하였습니다. (약 50번 가량)

상황을 가정하여 재부팅, 서비스 등록 후 실행을 각각 5번씩 반복해 본 결과 다음과 같이 나타났습니다.

1. 기존의 방식 (백그라운드 실행 직후 ps 그랩 > pid파일) 의 경우
- 재부팅 후 실행시 100%의 확률로 pid 생성 X
- 그 이후 실행시 100% 확률로 pid 생성 O

2. 현재 방식 (백그라운드 실행 직후 sleep 1을 준 후에 ps 그랩 > pid파일) 의 경우
- 재부팅 후 실행시 100%의 확률로 pid 생성 O
- 그 이후 실행시 100%의 확률로 pid 생성 O

3. 쉘 내장 변수 $! 사용시
- 재부팅 후, 그 이후 모든 상황에서 100% 확률로 pid 생성 O

결론은 위에서 댓글 달아주신 캐쉬의 문제일 확률이 높을 것 같기도 합니다. 그리고 해결책은 위엣분과tweedledum님께서 말씀하신 $! 가 가장 확실할 것 같습니다.^^

감사합니다.

tweedledum의 이미지

두번째 job 이 백그라운드 잡이 아니기 때문에 두번째 fork()는 없을 것 같네요. 두번째 fork() 마 빼고 생각하면 나머지 상황은 동일합니다.

어떤 캐쉬를 말씀하시는지 잘 모르겠지만. 근본적인 원인은 fork()와 exec()이 분리되서 실행되기 때문에 발생하는 문제가 맞습니다. 그렇다고 fork()와 exec()이 잘못 설계되었다는 얘긴 아닙니다.

http://en.wikipedia.org/wiki/Race_condition

A race condition or race hazard is the behavior of an electronic or software system where the output is dependent on the sequence or timing of other uncontrollable events. It becomes a bug when events don't happen in the order that the programmer intended. The term originates with the idea of two signals racing each other to influence the output first.

그리고 race condition 에 포함되는 문제가 맞습니다.

1. 두 job 이 순서에 의존한다.
2. 커널이 스케줄링을 하기 때문에 어떤 job 이 먼저 실행되는지 알 수 없다.
3. 님이 의도했던 순서는 a. background job, b. ps 였습니다.

morolty의 이미지

java 명령어가 백그라운드로 실행 됐을 때, fork가 일어나고, 본래 스크립트와 fork로 분기된 스크립트가 동시에 진행 되는게 맞나요?

그러면 ps를 실행 시킨 exec()와 java를 실행시킨 exec()가, 실행시작부터 끝날때까지의 수행시간(?) 이 다르다는 의미인가요, 아니면 두 exec()의 실행시간은 같은데 cpu 사용 우선순위가 다르다는 의미인가요?

그렇다면 2.에서 말씀하신 어떤 job이 먼저 실행되는지 알 수 없다는 뜻이 "그때 그때 실행할 때마다 다르다" 가 아닌 "java가 우선순위가 높을지 ps가 우선순위가 높을지 우리는 알 수 없다." 라는 의미가 되는건가요?

자꾸 질문해서 죄송합니다.. 바쁘지 않으실때 답변해주시면 감사드리겠습니다...

tweedledum의 이미지

네... java 프로세스를 background로 실행하기 위해 쉘이 fork()를 호출해야 합니다. 그래서 쉘의 본래 프로세스와 분기된 프로세스가 동시에 진행되게 됩니다.

수행시간은 중요하지 않습니다. fork()가 된 순간... 한개의 프로세스가 두 개의 프로세스로 변하게 됩니다. multi core 프로세서에서는 어떨지 생각해보질 않았는데요(커널이 멀티 코어를 다루는 원리를 잘 모릅니다. 구세대 사람이고 프로그램 세계를 떠난지 좀 되었네요. 치킨집을 하는 건 아닙니다.). 일단 단일 프로세서에서 생각해보면 쉬울 것입니다. 어떤 순간에 보면 딱 하나의 프로세스만이 실행되는 것입니다.

네 맞습니다. fork() 후부터는 어떤 프로세스가 우선순위가 높을지 예측할 수 없습니다. 그리고 그 우선순위는 계속 바뀝니다. 굉장히 짧은 시간에... 어떤 프로세스가 우선순위가 높을지 모른다는 얘기는 자바가 먼저 실행될지 ps가 먼저 실행될지 유저 입장에서는 알 수 없다는 뜻입니다.

tweedledum의 이미지

좌측이 차일드고 우측이 페어런트(쉘이겠죠)입니다. 세로는 시간 축입니다.

다음과 같이 두 가지 케이스가 발생할 수 있습니다.

1. 첫번째 케이스

fork(); // fork
exec(java_process); // child of first fork
                                        exec(ps); // parent

2. 두번째 케이스

fork(); // fork
                                        exec(ps); // parent
exec(java_process); // child of first fork

다른 쓰레드에 fork()관련된 질문이 있던데요. fork()는 프로세스를 통째로 복사하는 작업입니다. fork() 직후에는 프로세스 pid를 제외하곤 두 프로세스에 아무런 차이가 없습니다. 즉 이때부터 커널은 두 개의 프로세스를 다루게 되는 것이지요. 이 두개의 프로세스는 커널입장에서 보았을 때 동일한 자격을 가지고 있습니다. 입출력 대기 상태로 빠지거나 혹은 실행 대기 상태에서 얼마나 오랬동안 기다렸냐에 따라 실행을 위한 우선순위 값이 바뀌게 됩니다. 쉽게 얘기하면 사용자 입장에서는 어떤 게 먼저 실행되느냐 예측할 수 없다는 뜻입니다.

그리고 exec(java_process)를 수행하고 나서야 process 이름이 바뀌어 있겠죠.

첫번째 케이스를 살펴보면 님이 의도한 결과를 얻을 수 있을 겁니다. ps가 실행될 때 어떤 PID 값을 가진java_process가 생성되어 실행되고 잇을 테니까요.

하지만 두번째 케이스는 조금 다릅니다.
pid를 가진 백그라운드 프로세스를 위해서 fork()를 수행했지요. 하지만 java_process가 실행되기 전에 ps 가 실행되기 때문이지요.

그래서 어떤 작업이 먼저 수행되느냐에 따라 ps 로 java_proces가 출력될 수도 그렇지 않을 수도 있는 상황이 발생하는 것입니다.

코딩을 한지 오래되어 쉘 프로그램에서 어떻게 행동할까가 조금 헷갈리게 되는데... ps 작업을 위해서도 쉘은 fork()를 수행해야할 것 같습니다. 하지만 그 프로세스가 수행될때까지 부모 프로세스는 기다려야 할테니까(waitpid 류의 함수?) fork()를 안하는 경우와 동일해집니다. 심플하게 ps를 위해 fork()를 하지 않는 걸로 생각하는게 이해하기 쉬울 것입니다.

그리고... 실제로 실행 프로세스 커널에서 교체되는 때는 프로세스의 수행 루틴이 오로지 커널 내에 있을 때만 가능합니다. 엄밀히 말하면 위 다이어그램이 정확하지는 않다는 뜻입니다. 하지만 틀렸다고 할 수는 없습니다. 아마도 정확하게 그린다면 이해하기는 더 어려워지겠지요.

현실에서 레이스 컨디션 문제가 나타날 때는 대개 한쪽으로 편중되어 나타납니다. 잘 동작하다가 어느 한 순간 배신을 때리지요. 만약 그렇지 않고 50:50 정도의 비중으로 나타나는 문제라면 쉽게 발견하겠지요. 쉽게 발견되니까 쉬운 겁니다.

=======================================================================================

쉘 내장 환경 변수를 출력하는 경우를 생각해보면...

1. 첫번째 케이스

fork(); // fork
exec(java_process); // child of first fork
                                        register internal environment var
                                        exec(echo $!); // parent

2. 두번째 케이스

fork(); // fork
                                        register internal environment var
                                        exec(echo $!); // parent
exec(java_process); // child of first fork

부모 프로세스는 fork() 시 자식 프로세스의 pid를 리턴 값으로 받습니다. 아마 쉘은 이 값을 받아서 내장 환경 변수로 등록하겠지요.

따라서 ps 의 경우와 같은 상황을 고려한다고 하더라도 exec(java_process)가 실행되었는지 여부에 관계없이 parent는 자식 프로세스의 pid를 출력할 수 있게 되는 것입니다.

"register internal environment var" 와 exec(echo $!) 사이에 exec(java_process)가 실행되는 경우는 그리지 않았는데... environment 변수를 다루는 함수는 시스템 콜을 포함하고 있지 않을 것으로 생각되어 그리하였습니다. 그리고 "register internal environment var" 와 exec(echo $!) 사이에 exec(java_process)가 실행된다고 하더라도 그 결과에는 변화가 없습니다.

morolty의 이미지

대충 이렇게 될 거라고 생각하고 있었는데 직접 정리해주셔서 이해되었습니다 ^^

답변 감사드립니다!

tweedledum의 이미지

곰곰히 생각해보니 한 가지 잘못 말한 게 있네요.

실행 프로세스 전환이 되는 시점은... 반드시 프로세스가 시스템 콜을 호출해서 커널로 진입해야만 발생하는 것은 아니겠네요. 예를 들어 어떤 인터럽트가 발생했을 때 커널에서 인터럽트 서비스를 해줘야 합니다. 이때 실행되던 프로세스의 컨텍스트를 저장해줘야만 하겠네요. 그러면 스케줄러 입장에서는 모든 프로세스의 컨텍스트가 저장되어 있기 때문에 우선순위가 높은 프로세스를 다시 선택할 기회를 가질 수 있겠지요. 리눅스 커널은 preemptive kernel 입니다.

예전에 윈도우 3.1 기억을 되살려보면... 잘못된 프로그램이 있으면 시스템이 멈춰버려서 재부티을 해야 했습니다. 프로세스 전환이 일어나지 않기 때문에 발생하는 문제였겠지요. 윈도우는 자발적으로 어떤 프로세스가 잠들러 가는 순간에만 다른 프로세스로 전환할 수 있다는 말이 기억이 나네요. 아마 그 때 스케줄러를 호출했던 거겠지요.

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.