두근두근이야기

linux pid관리 본문

IT/IT ::Advanced SystemProgramming

linux pid관리

골든 2013. 4. 27. 20:44

PID는 각 프로세스/스레드를 구분해 주는 번호이다.
(이후 특별히 구분할 필요가 없는 경우 단순히 태스크라고 부를 것이다.)
각 태스크는 부모-자식 관계와 더불어 다음과 같은 여러 가지 형태로 관련되어 있다.

  • 한 프로세스('스레드 그룹'이라고도 한다)에 속한 여러 스레드
  • 한 프로세스 그룹에 속한 여러 프로세스
  • 한 세션 그룹에 속한 여러 프로세스

부모-자식 관계는 task_struct를 통해 직접적으로 리스트로 연결되어 있으며 (children, sibling)
한 프로세스에 속한 여러 스레드들도 리스트로 직접 연결되어 있다. (thread_group)
하지만 프로세스 그룹이나 세션 그룹의 경우에는 그룹 리더의 PID를 통해 연결되기 때문에
이들 간의 관계를 관리하기 위한 자료 구조가 필요하다.
참고로 이러한 PGID와 SID는 프로세스 단위로 관리된다는 것을 주의하자.

이러한 PID들을 구분하기 위해 다음과 같은 상수를 정의한다.

enum pid_type
{
PIDTYPE_PID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX
};


PID 관리를 위해 사용되는 자료 구조는 바로 pid 구조체이며 (pid_t와는 다르다!)
pid 구조체는 task_struct와의 양방향 연결을 가진다.

struct pid
{
atomic_t count;
unsigned int level;
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX];
struct rcu_head rcu;
struct upid numbers[1];
};


일단 중요하게 살펴봐야 할 것은 tasks 배열이다.
이는 해당 pid에 연관된 태스크들의 리스트를 관리하기 위해 필요하다.
또한 각 태스크는 자신의 pid를 참조하기 위해 다음과 같은 자료 구조를 포함한다.

struct pid_link
{
struct hlist_node node;
struct pid *pid;
};


task_struct는 PIDTYPE_MAX 개 만큼의 배열로 pids라는 필드를 가진다.
모든 태스크가 각자의 pid를 가지므로 각 태스크 생성 시 do_fork()에서 호출하는 copy_process() 함수는
pid 구조체를 생성하고 이를 아래의 그림과 같이 해당 태스크에 연결한다.


프로세스 그룹과 세션 그룹의 경우에는 여러 프로세스가 관련되어 있으므로
pid_link 구조체의 node 필드를 통해 리스트로 연결된다.
아래의 그림에서 "B" 태스크가 프로세스 그룹의 리더이며 "A" 태스크는 해당 그룹의 멤버이다.
그림에는 빠져있지만 세션 그룹의 경우도 동일하게 연결될 것이다.


하지만 여기까지는 우리가 실제로 사용하는 PID에 해당하는 번호가 포함되지 않았다.
이는 namespace의 개념이 등장하면서 좀 더 복잡해지기 때문에 upid라는 구조체로 분리되어 관리된다.
namespace에 대한 설명은 잠시 미루어두고 우선 upid 구조체를 먼저 살펴보기로 하자.

struct upid {
int nr;
struct pid_namespace *ns;
struct hlist_node pid_chain;
};


nr 필드가 우리가 알고있는 PID 번호에 해당하는 정보가 저장된다.
ns 필드는 해당 PID가 속한 namespace 정보이다.
pid_chain 필드는 PID 해시 테이블에서 같은 인덱스에 속한 upid들을 연결할 때 사용된다.

PID 번호를 통해 해시 테이블을 검색하면 그에 해당하는 upid 구조체를 찾을 수 있으며
container_of() 매크로를 통해 이를 포함하는 pid 구조체를 찾을 수 있고 따라서 task_struct 구조체도 찾을 수 있다.
이러한 해시 테이블은 pid_hash 변수를 통해 접근할 수 있는데
실제로 해시 테이블을 검색하기 위해서는 PID 번호 뿐 아니라 namespace 정보도 필요하다.

PID namespace는 2.6.24 버전부터 지원되는데
기본적으로는 init_pid_ns라는 전역 namespace를 사용하지만
clone(CLONE_NEWPID) 시스템 콜을 통해 새로운 PID namespace를 생성하면 별도의 PID를 관리할 수 있게 되며,
새로운 namespace와 기존의 namespace에서 같은 PID를 가지는 프로세스는 서로 아무런 관련이 없다.

PID namespace는 계층적으로 관리되며
자식 namespace의 PID들은 (비록 PID 번호는 다르지만) 부모 namespace에 매핑된다.
PID 해시 테이블은 모든 namespace 내의 PID들을 한꺼번에 관리하므로
PID 번호를 통해 태스크를 검색하기 위해서는 namespace 정보도 함께 사용해야 한다.

이제 다시 pid 구조체를 살펴보면
level 필드는 해당 PID가 속한 namespace의 depth를 나타내는 값이다.
또한 numbers 배열은 기본적으로는 하나의 원소 만을 포함하지만
새로운 namespace가 생길 때 마다 원소가 하나씩 추가된다.

예를 들어 새로운 PID namespace를 생성하면 해당 프로세스의 PID는 1로 설정되는데
부모 namespace에서는 해당 프로세스가 4321이라는 PID를 가진다고 하면 각 필드의 값은 다음과 같을 것이다.

pid.level = 1;
pid.numbers[0].nr = 4321;
pid.numbers[0].ns = &init_pid_ns;
pid.numbers[1].nr = 1;
pid.numbers[1].ns = &new_pid_ns; /* = current->nsproxy->pid_ns */


이 때 0번 namespace 즉, init_pid_ns를 통해 할당된 PID 번호를 global PID 번호라고 하며
현재 사용 중인 namespace에서 할당된 PID 번호는 virtual PID 번호라고 한다.
새로운 namespace가 생성되지 않았다면 이 둘은 같다.

이제 이들을 사용하기 위한 커널 API들을 살펴보도록 하자.

  • task_pid() : 태스크의 PID 정보를 가지고 있는 pid 구조체를 반환
  • pid_task() : pid 구조체와 연관된 첫 번째 태스크 구조체를 반환

pid_task() 함수의 경우 pid_type을 지정하여 원하는 태스크를 선택할 수 있지만
task_pid() 함수는 각각을 위한 전용 함수가 따로 존재한다.
(아마도 스레드 그룹 리더를 통하지 않는 실수를 방지하기 위함인 듯 하다.)

  • task_tgid() : 스레드 그룹 리더의 PID (즉, 프로세스 ID) 정보를 가지는 pid 구조체를 반환
  • task_pgrp() : 프로세스 그룹 리더의 PID 정보를 가지는 pid 구조체를 반환
  • task_sid() : 세션 리더의 PID 정보를 가지는 pid 구조체를 반환

pid 구조체로부터 실제 PID 번호를 가지고 오는 함수들은 다음과 같다.

  • pid_nr() : global PID 번호를 반환
  • pid_vnr() : virtual PID 번호를 반환
  • pid_nr_ns() : 지정된 ns(namespace)에 속한 PID 번호를 반환

반대로 주어진 PID 번호로부터 pid 구조체를 얻기 위한 함수들은 다음과 같다.

  • find_vpid() : virtual PID 번호에 해당하는 pid 구조체를 반환
  • find_pid_ns() : 지정된 ns에 속한 PID 번호에 해당하는 pid 구조체를 반환
  • find_get_pid() : find_vpid()와 동일하지만 pid 구조체의 참조 카운트를 증가시킴

(이상하게도?) global PID 번호를 이용하여 검색하는 함수는 없는 듯 하다.
위의 함수와 pid_task() 함수를 함께 이용하면 항상 원하는 태스크를 찾을 수 있다.
다음과 같은 함수를 이용하면 한 번에 동일한 작업을 수행할 수도 있다.
(하지만 export되지 않아서 모듈에서는 이용할 수 없을 것이다.)

  • find_task_by_vpid() : virtual PID 번호에 해당하는 태스크 구조체를 반환
  • find_task_by_pid_ns() : 지정된 ns에 속한 PID 번호에 해당하는 태스크 구조체를 반환

미자막으로 namespace 기능을 확인하기 위해 다음과 같은 간단한 예제 프로그램을 실행해 보았다.

new_pid_ns.c:

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>


int new_pid_ns(void *data)
{
execlp("bash", NULL, NULL);
return -1;
}

int main(void)
{
int status;
long stack[4096];

if (clone(new_pid_ns, &stack[4095], SIGCHLD | CLONE_NEWNS | CLONE_NEWPID, NULL) == -1) {
perror("clone");
return -1;
}

wait(&status);
}


그리고 PID 번호를 확인하기 위한 간단한 커널 모듈을 작성한다.
물론 커널 설정 시 CONFIG_PID_NS가 선택되어야 한다.

pid-test.c:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/sched.h>

MODULE_LICENSE("GPL");

static int npid = 1;
module_param(npid, int, 0644);

static int mod_init(void)
{
int i, error = 0;
struct task_struct *p;
struct pid *pid;

rcu_read_lock();
pid = find_get_pid(npid);
p = pid_task(pid, PIDTYPE_PID);
if (!p) {
error = -EINVAL;
goto out;
}
printk("%s: pid level = %u\n", p->comm, pid->level);
for (i = 0; i <= pid->level; i++)
printk("[%d] %d\n", i, pid->numbers[i].nr);
out:
put_pid(pid);
rcu_read_unlock();
return error;
}

static void mod_exit(void)
{
}

module_init(mod_init);
module_exit(mod_exit);


이제 컴파일하고 실행해보면 다음과 같은 결과를 볼 수 있다.
(좀 더 확실한 결과를 보기 위해 PID namespace를 3번 생성해 보았다.)
참고로 PID namespace 생성 후 /proc을 다시 마운트해야만 원하는 결과를 얻을 수 있다!

# ./new_pid_ns
# ./new_pid_ns
# ./new_pid_ns
# mount -t proc nodev /proc
# ps
PID TTY TIME CMD
1 ? 00:00:00 bash
3 ? 00:00:00 ps
# insmod pid-test.ko
bash: pid level = 3
[0] 728
[1] 5
[2] 3
[3] 1

 

http://studyfoss.egloos.com/5242243