프로세스에 대한 이해를 하려면 프로세스가 어떻게 생성되는 지 알면 좋습니다. 프로세스 생성 과정에서 프로세스를 관리하는 자료구조 관계를 알 수 있기 때문입니다. 

리눅스에서 구동되는 프로세스는 크게 유저 레벨에서 생성된 프로세스와 커널 레벨에서 생성된 프로세스가 있습니다. 

유저 레벨에서 생성된 프로세스는 유저 공간에서 프로세스를 생성하는 라이브러리(glibc) 도움을 받아 커널에게 프로세스 생성 요청을 합니다. 커널 프로세스는 kthread_create() 함수를 호출해서 커널 내부에서 프로세스를 생성합니다. 커널 프로세스는 커널 스레드라고 부르며 커널 내부에서 스레드를 직접 관리합니다.

공통으로 리눅스에서 생성된 프로세스는 _do_fork() 함수를 호출합니다. 프로세스 생성하는 핵심함수는 _do_fork() 이니 이 함수를 중심으로 프로세스가 어떻게 생성되는지 알아봅시다.

_do_fork() 함수 소개

리눅스에서 구동 중인 모든 프로세스는 _do_fork() 함수가 실행할 때 생성됩니다. 프로세스는 누가 생성할까요? 리눅스 시스템에서 프로세스 생성을 전담하는 프로세스가 있습니다. 주인공은 init과 kthreadd 프로세스입니다.

유저 레벨 프로세스는 init 프로세스, 커널 레벨 프로세스(커널 스레드)는 kthreadd 프로세스가 생성하는 것입니다. 프로세스 생성 과정에 대해서 조금 더 정확히 말하면 프로세스는 생성이 아니라 복제된다고 설명할 수 있습니다.

프로세스를 생성할 때 여러 리소스(메모리 공간, 페이지 테이블, 가상 메모리 식별자)를 커널로부터 할당 받아야 합니다. 프로세스 동작에 필요한 리소스를 각각 할당 받으면 시간이 오래 걸리니 이미 생성된 프로세스에서 복제하는 것입니다.


리눅스 커널에서는 속도 개선을 위해 반복해서 실행하는 코드를 줄이려는 노력을 한 흔적을 볼 수 있습니다. 커널 메모리 할당자인 슬럽 메모리 할당자(Slub Memory Allocator)도 유사한 역할을 수행합니다. 드라이버에서 자주 메모리 할당 요청을 하는 쓰는 구조체를 정의해서 해당 구조체에 대한 메모리를 미리 확보해 놓습니다. 메모리 할당 요청 시 바로 이미 확보한 메모리를 할당하는 속도가 빠르기 때문입니다.


프로세스 생성 과정도 마찬가지입니다. 프로세스를 생성할 때 이미 생성된 프로세스에서 복제하는 것이 더 효율적입니다. 따라서 모든 프로세스는 부모와 자식 프로세스를 확인할 수 있습니다.

먼저 _do_fork() 함수 선언부를 보면서 이 함수에 전달되는 인자와 반환값을 확인합시다.

[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/fork.c#L2020]
extern long _do_fork(unsigned long, unsigned long, unsigned long, int __user *, int __user *, unsigned long);

long _do_fork(unsigned long clone_flags,
      unsigned long stack_start,
      unsigned long stack_size,
      int __user *parent_tidptr,
      int __user *child_tidptr,
      unsigned long tls);

먼저 반환값을 확인합시다.
함수 선언부와 같이 반환값 타입은 long이며 PID를 반환합니다. 프로세스 생성 시 에러가 발생하면 PTR_ERR() 매크로로 지정된 에러 값을 반환합니다.

_do_fork() 함수에 전달하는 인자값들을 점검합시다.

unsigned long clone_flags;

프로세스를 생성할 때 전달하는 매크로 옵션 정보를 저장합니다. 이 멤버에 다음 매크로를 저장합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/uapi/linux/sched.h]
#define CSIGNAL 0x000000ff /* signal mask to be sent at exit */
#define CLONE_VM 0x00000100  
#define CLONE_FS 0x00000200  
#define CLONE_FILES 0x00000400  
#define CLONE_SIGHAND 0x00000800  
#define CLONE_PTRACE 0x00002000 
#define CLONE_VFORK 0x00004000 
#define CLONE_PARENT 0x00008000 
#define CLONE_THREAD 0x00010000

unsigned long stack_start;

보통 유저 영역에서 스레드를 생성할 때 복사하려는 스택 주소입니다. 이 스택 주소는 유저 공간에서 실행 중인 프로세스 스택 주소입니다.

unsigned long stack_size;
보통 유저 영역 실행 중인 스택 크기입니다. 보통 유저 영역에서 스레드를 생성할 때 복사합니다.

int __user *parent_tidptr;
int __user *child_tidptr;

부모와 자식 스레드 그룹을 관리하는 핸들 정보입니다.

커널에서 _do_fork() 함수를 언제 호출할까요? 생성하려는 프로세스 유형에 따라 함수 호출 흐름이 나뉩니다.
 1. 유저 모드에서 생성한 프로세스: sys_clone() 시스템 콜 함수 
 2. 커널 모드에서 생성한 커널 스레드: kernel_thread() 함수
프로세스는 유저 모드에서 생성된 프로세스와 커널 모드에서 생성된 프로세스로 분류할 수 있습니다. 각각 유저 레벨 프로세스와 커널 레벨 프로세스라고 부릅니다.


유저 레벨 프로세스 생성 시 _do_fork() 함수 흐름 
먼저 유저 레벨 프로세스는 어떻게 생성할까요? 저수준 리눅스 어플리케이션 프로그램으로 fork() 함수를 호출하면 리눅스에서 제공하는 라이브러리 도움을 받아 커널에게 프로세스 생성 요청을 합니다. 여기까지가 유저 모드에서 프로세스를 요청하는 단계입니다.

리눅스에서 제공하는 라이브러리는 시스템 콜을 발생하고 리눅스 커널에서는 fork() 함수에 대응하는 시스템 콜 핸들러인 sys_clone() 함수를 호출합니다.

먼저 sys_clone() 함수 코드를 봅시다.
 [https://elixir.bootlin.com/linux/v4.14.70/source/kernel/fork.c#L2020]
1 #ifdef __ARCH_WANT_SYS_CLONE
2 #ifdef CONFIG_CLONE_BACKWARDS
3 SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
4  int __user *, parent_tidptr,
5  unsigned long, tls,
6  int __user *, child_tidptr)
...
7 #endif
8 {
9 return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
10 }
11 #endif

아래와 같이 시스템 콜 함수를 정의하면, sys_clone() 이란 시스템 콜 래퍼 함수를 생성합니다.
3 SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,

sys_clone() 함수는 _do_fork() 함수를 그대로 호출합니다.

마찬가지로 sys_fork()와 sys_vfork() 이란 시스템 콜 함수를 확인해봅시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/fork.c#L2020]
1 SYSCALL_DEFINE0(fork)
2 {
3 #ifdef CONFIG_MMU
4 return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
5 #else
6 /* can not support in nommu mode */
7 return -EINVAL;
8 #endif
9 }
10
11 SYSCALL_DEFINE0(vfork)
12 {
13 return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
14 0, NULL, NULL, 0);
15 }


sys_fork()와 sys_vfork() 함수도 역시 _do_fork() 함수를 그대로 호출합니다. 

정리하면 유저 공간에서 생성한 프로세스는 sys_clone(), sys_fork() 그리고 sys_vfork() 시스템 콜 함수를 통해서 _do_fork() 함수를 호출합니다.

그런데 유저 공간(리눅스 시스템 프로그래밍)에서 fork() 이란 함수로 프로세스를 생성하면 시스템 콜 함수로 sys_clone() 를 호출합니다. 예전 리눅스 커널 버전에서는 fork()을 쓰면 sys_fork() 함수를 호출했으나 최근 리눅스 커널에서는 sys_clone() 함수를 실행합니다. vfork() 시스템 콜 함수도 fork() 시스템 콜을 개선하기 위해 이전 리눅스 커널 버전에서 썼던 레거시(과거) 코드입니다.

유저 레벨에서 생성한 프로세스와 스레드를 커널에서 동등하게 처리합니다. 그러니 sys_clone() 함수를 호출하는 것입니다.

정리하면 유저 공간에서 fork() 란 함수를 호출하면 시스템 콜이 실행해어 커널 공간에서 sys_clone() 함수를 호출합니다. 

커널 프로세스 생성 시 do_fork() 함수 흐름 

커널 공간에서 시스템 리소스(메모리, 전원) 관리를 수행하는 프로세스를 커널 스레드라고 합니다. 커널 스레드는 어떻게 생성할까요? 다음과 같이 kthread_create() 함수에 적절한 인자를 전달하면 됩니다.

커널 스레드를 생성하는 코드를 같이 봅시다. 
[https://elixir.bootlin.com/linux/v4.14.70/source/drivers/vhost/vhost.c#L334]
1 long vhost_dev_set_owner(struct vhost_dev *dev)
2 {
3 struct task_struct *worker;
4 int err;
...
5 /* No owner, become one */
6 dev->mm = get_task_mm(current);
7 worker = kthread_create(vhost_worker, dev, "vhost-%d", current->pid);

위 7번째 줄 코드에서 kthread_create() 함수를 실행하면 커널 스레드를 생성하는 것입니다. kthread_create() 이란 함수를 호출하면 커널 스레드 생성을 담당하는 kthreadd란 프로세스에게 커널 스레드 생성 요청을 합니다. 이후 kthreadd 스레드는 _do_fork() 함수를 실행해서 프로세스를 생성합니다. 

커널 스레드도 프로세스의 한 종류라 볼 수 있습니다.

대부분 커널 스레드는 시스템이 부팅할 때 생성하며 리눅스 커널이 커널 스레드가 필요할 때 동적으로 생성합니다. 예를 들어 리눅스 드라이버에서 많은 워크를 워크큐에 큐잉하면 커널 스레드의 종류인 워커 스레드를 더 생성해야 합니다. 시스템 메모리가 부족할 경우 메모리를 회수해서 가용 메모리를 확보하는 커널 스레드를 생성할 때도 있습니다. 보통 시스템이 더 많은 일을 해야 할 때 커널 스레드를 생성합니다.

프로세스 생성에 대한 소개를 했으니 다음에 라즈베리파이로 유저 공간에서 실행 중인 프로세스를 점검해봅시다.



#Reference

프로세스 소개

  | 프로세스란/ 태스크란/ 스레드란

프로세스 확인하기

프로세스는 어떻게 생성하나?

  | do_fork() 함수 소개

  | 유저 레벨 프로세스 생성 시 _do_fork() 함수 흐름

리눅스 시스템 개발자(디바이스 드라이버, 데브옵스)로 오래 동안 실력을 인정 받으려면 리눅스 커널을 잘 알면 좋습니다. 하지만 리눅스 커널은 그 내용이 방대하고 깊이가 있어 단기간에 익히기 어려운 기술 영역입니다. "프로세스란 무엇인가"란 질문으로 리눅스 커널을 시작합니다. 성경이나 불경같이 근엄한 단어를 많이 보입니다.  안타깝게도 20페이지 정도 읽다가 포기합니다. 너무 이론으로 프로세스를 설명하기 때문입니다.

프로세스에 익숙해지려면 리눅스 시스템에 익숙해져야 합니다. 이번 장에서는 라즈베리파이에서 명령어를 입력하고 ftrace 로그에서 출력되는 로그로 프로세스 동작을 확인합니다.

먼저 다음 리눅스 명령어로 시스템에서 프로세스 목록을 확인합시다. 이를 위해 라즈베리파이에서 x-terminal 프로그램을 실행해서 셸을 열어야 합니다. 
root@raspberrypi:/home/pi# ps -ely
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S     0     1     0  0  80   0  5956  6991 SyS_ep ?        00:00:02 systemd
S     0     2     0  0  80   0     0     0 kthrea ?        00:00:00 kthreadd
...
S  1000   867   517  0  80   0  7720 12887 poll_s ?        00:00:00 gvfsd-trash
S  1000   876   730  0  80   0 20084 12108 poll_s ?        00:00:07 lxterminal
S  1000   877   876  0  80   0  1324   590 unix_s ?        00:00:00 gnome-pty-helpe
S  1000   878   876  0  80   0  4028  1628 wait   pts/0    00:00:00 bash
S     0   886   878  0  80   0  3380  1908 poll_s pts/0    00:00:00 sudo
S     0   890   886  0  80   0  3076  1818 wait   pts/0    00:00:00 su
리눅스 시스템에서 프로세스 목록을 보기 위해서는 "ps"라는 명령어를 입력하면 됩니다.

x-terminal 셸을 실행한 상태에서 "info ps" 명령어를 입력하면 ps 명령어의 의미를 알 수 있습니다.

-------
PS(1)                                   User Commands                                   PS(1)

NAME
       ps - report a snapshot of the current processes.

SYNOPSIS
       ps [options]

리눅스 시스템에서 돌고 있는 프로세스를 출력하는 명령어입니다. 리눅스 시스템에서 디버깅을 할 때 많이 쓰는 명령어이니 자주 활용합시다.

리눅스 시스템에서 생성된 모든 프로세스(유저 레벨, 커널 스레드)는 init 프로세스를 표현하는 전역 변수 init_tasks.next 멤버에 연결 리스트로 등록돼 있습니다. ps 명령어를 입력하면 이 연결 리스트를 순회하면서 프로세스 정보(struct task_struct)를 얻어 프로세스 정보를 출력하는 겁니다.

이번에는 ps 명령어에 "-ejH" 이란 옵션을 주고 프로세스를 부모 자식 프로세스 관계로 출력합시다.
1 root@raspberrypi:/home/pi # ps -ejH
2   PID  PGID   SID TTY          TIME CMD
3    2     0     0 ?        00:00:00 kthreadd
4    4     0     0 ?        00:00:00   kworker/0:0H
5    6     0     0 ?        00:00:00   mm_percpu_wq
6    7     0     0 ?        00:00:00   ksoftirqd/0
...
7  17103     0     0 ?     00:00:00   kworker/1:1
8  17108     0     0 ?     00:00:00   kworker/u8:0
9     1     1     1 ?        00:00:02 systemd
10   94    94    94 ?        00:00:00   systemd-journal
11  127   127   127 ?        00:00:00   systemd-udevd
12  274   274   274 ?        00:00:00   systemd-timesyn

4~6번 줄에 보이는 "kworker/0:0H", "mm_percpu_wq" 그리고 "ksoftirqd/0" 이란 프로세스의 부모 프로세스는 3번 줄에 있는 "kthreadd" 입니다. 

pid가 2이 "kthreadd" 프로세스는 커널 공간에서 실행 중인 프로세스를 생성하는 역할을 수행합니다. 위 출력 결과에서 4~8번 줄에 있는 프로세스들은 같은 행으로 정렬돼 있습니다. 이 목록에서 보이는 프로세스를 커널 스레드, 커널 프로세스라고 합니다. 커널 공간에서만 실행합니다. 

리눅스 커널에서는 프로세스 마다 PID(Process id)라는 int 형 ID를 부여합니다.
swapper 프로세스는 PID가 0이고 init 프로세스는 PID가 1 그리고 커널 스레드를 생성하는 kthreadd 프로세스는 PID가 2입니다.
새로운 프로세스를 생성할 때 커널이 부여하는 PID 정수값은 증가합니다. PID로 프로세스가 언제 생성됐는지 추정할 수 있습니다.

PID는 최댓값은 32768로 정해져 있습니다.

이번에는 9번째 줄 로그를 봅시다. pid가 1인 systemd 프로세스가 보입니다.
9     1     1     1 ?        00:00:02 systemd

pid가 1인 프로세스를 임베디드 리눅스에서는 init 프로세스라고 하며 모든 유저 공간에서 생성된 프로세스의 부모 프로세스 역할을 수행합니다.

프로세스는 인간을 객체화해서 고안한 내용이 많습니다. 프로세스는 각자 부모 자식 프로세스들이 있고 자식 프로세스가 종료할 때 부모 프로세스에게 신호를 알립니다.

만약 조부모, 부모, 자식 프로세스가 있다고 가정합니다. 예외 상황으로 부모 프로세스가 종료되면 자식 프로세스 입장에서 부모 프로세스가 사라집니다. 이 때 조부모가 부모 프로세스가 됩니다. 이런 상황에서 init 프로세스가 조부모 역할(새로운 부모 프로세스)을 수행합니다.

다음에 리눅스 커널 소스 코드를 열어서 프로세스를 생성할 때 어떤 함수가 실행하는지 살펴봅시다.


프로세스는 추상적이고 다양한 의미를 담고 있어 다양한 관점으로 설명할 수 있습니다.

프로세스란 무엇일까요? 프로세스(Process)는 리눅스 시스템 메모리에서 실행 중인 프로그램을 말합니다. 스케줄링 대상인 태스크와 유사한 의미로 쓰입니다. 다수 프로세스를 실시간으로 사용하는 기법을 멀티프로세싱이라고 말하며 같은 시간에 멀티 프로그램을 실행하는 방식을 멀티태스킹이라고 합니다.

우리가 쓰고 있는 스마트폰 동작을 잠깐 생각해봅시다. 
전화를 하면서 메모를 남기고, 음악을 들으면서 브라우저를 볼 수 있습니다. 여러 어플리케이션이 동시에 실행하고 있습니다. 이것은 멀티태스킹을 수행해서 프로그램을 시분할 방식으로 처리하기 때문에 가능합니다.

이번에는 리눅스 개발자 입장에서 프로세스에 대해 생각해 봅시다. 프로세스는 리눅스 시스템 메모리에 적재되어 실행을 대기하거나 실행하는 실행 흐름을 의미합니다. 프로세스가 실행을 대기한다면 실행할 때 어떤 과정을 거칠까요? 프로세스는 어떤 구조체로 식별할까요? 다양한 의문이 생깁니다.

프로세스를 관리하는 자료구조에자 객체를 태스크 디스크립터라고 말하고 구조체는 struct task_struct 입니다. 이 구조체에 프로세스가 쓰는 메모리 리소스, 프로세스 이름, 실행 시각, 프로세스 아이디(PID), 프로세스 스택 최상단 주소가 저장돼 있습니다.

프로세스를 struct task_struct 이란 구조체로만 표현할 수 있을까요? 위에서 프로세스란 실행 흐름 그 자체라고 정의했습니다. 프로세스 실행 흐름은 어느 구조체에 저장할 수 있을까요?

프로세스는 실행할 때 리눅스 커널 함수를 호출합니다. 임베디드 리눅스 디버거의 전설인 Trace32 프로그램으로 콜 스택을 하나 봅시다.
1 -000|__schedule()
2 -001|schedule_timeout()
3 -002|do_sigtimedwait()
4 -003|sys_rt_sigtimedwait()
5 -004|ret_fast_syscall(asm)

위 함수 호출 방향은 5번째 줄에서 1번째 줄입니다. 콜 스택을 간단히 해석하면 유저 공간 프로그램에서 sigtimedwait() 이란 함수를 호출하면 이에 대응하는 시스템 콜 핸들러 함수인 sys_rt_sigtimedwait() 함수 실행 후 스케줄링되는 함수 흐름입니다.

프로세스는 함수를 호출하면서 실행을 합니다. 그런데 함수를 호출하고 실행할 때 어떤 리소스를 쓸까요? 프로세스 스택 메모리 공간입니다.

모든 프로세스들은 커널 공간에서 실행할 때 각자 스택 공간을 할당 받으며 스택 공간에서 함수를 실행합니다. 

위에서 본 프로세스가 스케줄러에 의해 다시 실행한다고 가정합시다. 그럼 어떻게 실행할까요?
1 -000|__schedule()
2 -001|schedule_timeout()
3 -002|do_sigtimedwait()
4 -003|sys_rt_sigtimedwait()
5 -004|ret_fast_syscall(asm)

1번 함수에서 5번 함수 방향으로 되돌아 올 겁니다. 이는 이 프로세스가 마지막에 실행했던 레지스터 세트와 실행 흐름이 프로세스 스택 공간에 저장돼 있었기 때문입니다.

프로세스를 실행 흐름을 표현하는 또 하나 중요한 공간은 프로세스 스택 공간이며 이 프로세스 스택 최상단 주소에 struct thread_info 란 구조체가 있습니다.

정리하면 프로세스는 추상적인 개념이지만 프로세스 정보와 프로세스 실행 흐름을 저장하는 구조체와 메모리 공간이 있습니다. 리눅스 커널에서 실시간으로 구동하는 프로세스에 대해 잘 알려면 이 자료구조를 잘 알 필요가 있습니다.

우리가 열심히 분석하는 리눅스 커널 소스 코드를 실행하는 주체가 프로세스이며 프로세스 스택 공간에서 실행하는 것입니다.

태스크란
태스크는 무엇일까요? 태스크는 리눅스 이외 다른 운영체제에서 예전부터 쓰던 용어입니다.
운영체제 이론을 다루는 예전 이론서는 대부분 태스크란 단어를 많이 볼 수 있습니다.

태스크는 운영체제에서 어떤 의미일까요? 말 그대로 실행(Execution)이라 할 수 있습니다.
운영체제 책들을 보면 첫 장에서 태스크에 대한 설명을 볼 수 있습니다. 최근 운영 체제에서는 대부분 기본으로 멀티 태스킹 환경에서 프로그램을 실행하나 예전에는 특정 코드나 프로그램 실행을 일괄 처리했습니다. 이 실행 및 작업 단위를 태스크라고 불렀습니다.


화면이 없는 간단한 시나리오의 임베디드 시스템에서는 태스크 2개로 서로 시그널을 주고 받으며 시스템 전체를 제어할 수 있습니다. 

하지만 태스크에 대한 개념은 현재 프로세스와 겹치는 부분이 많습니다. 태스크에 대한 의미가 프로세스와 스레드에 대한 개념이 도입하면서 발전했습니다. 태스크를 실행하는 단위인 실행(Execution)을 결정하는 기준이 스케줄링으로 바뀐 겁니다. 

예전에 쓰던 용어를 현재 소프트웨어에 그대로 쓰는 경우가 많습니다. 이를 레거시(Legacy)라고 말하고 과거 유물이란 뜻도 있습니다. 예전에 썼던 태스크란 용어를 리눅스 커널 용어나 소스 코드에서 그대로 쓰고 있습니다. 프로세스 속성을 표시하는 구조체 이름을 struct task_struct으로 쓰고 있습니다. 

프로세스 마다 속성을 표현하는 struct task_struct 구조체는 태스크 디스크립터라고 하며 프로세스 디스크립터라고도 말합니다.

리눅스 커널 함수 이름이나 변수 중에 task란 단어가 보이면 프로세스 관련 코드라 생각해도 좋습니다.

예를 들어 다음 함수는 모두 프로세스를 관리 및 제어하는 역할을 수행하며 함수 이름에 보이는 태스크는 프로세스로 바꿔도 무방합니다.
dump_task_regs
get_task_mm
get_task_pid
idle_task
task_tick_stop

리눅스 커널에서 태스크는 프로세스와 같은 개념으로 쓰는 용어입니다. 소스 코드나 프로세스에 대한 설명을 읽을 때 태스크란 단어를 보면 프로세스와 같은 개념으로 이해합시다.

스레드란
스레드는 무엇일까요? 간단히 말하면 유저 레벨에서 생성된 가벼운 프로세스라 말할 수 있습니다. 멀티 프로세스 실행 시 컨택스트 스위칭을 수행해야 하는데 이 때 비용(시간)이 많이 듭니다. 실행 중인 프로세스의 가상 메모리 정보를 저장하고 새롭게 실행을 시작하는 프로세스도 가상 메모리 정보를 로딩해야 합니다. 또한 스레드를 생성할 때는 프로세스를 생성할 때 보다 시간이 덜 걸립니다.

스레드는 자신이 속한 프로세스 내의 다른 스레드와 파일 디스크립터, 파일 및 시그널 정보에 대한 주소 공간을 공유합니다. 프로세스가 자신만의 주소 공간을 갖는 것과 달리 스레드는 스레드 그룹 안의 다른 스레드와 주소 공간을 공유합니다.

하지만 커널 입장에서는 스레드를 다른 프로세스와 동등하게 관리합니다. 대신 각 프로세스 식별자인 태스크 디스크립터(struct task_struct)에서 스레드 그룹 여부를 점검할 뿐입니다.


양극화는 우리 사회가 겪고 있는 심각한 문제 중 하나다. 


양극화를 아주 이해하기 쉽게 설명하면, 

잘사는 가정에서 태어난 아이들은 평생 부유한 환경과 유리한 조건에서 살고 못사는 집안의 아이는 평생 라면만 먹고 산다는 것이다. 


양극화의 가장 큰 원인은 교육 기회의 박탈이다. 못사는 집안의 아이들은 부유한 가정의 아이들에 비교해 교육 환경이 좋지 못하니 좋은 대학에 가지 못하고 이로 좋은 회사에 취업도 못해 가난의 대물림이 계속된다는 것이다. 


이 문장을 임베디드 개발 양극화 관점으로 바꿔 표현해보자. 

좋은 개발 부서에서 실무 프로젝트를 수행한 개발자는 계속 실력이 향상하며 좋은 대우를 받으나 허접한 개발 부서에서 개발을 시작한 개발자는 개발 능력이 업그레이드되지 않아 평생 라면만 먹으며 낮은 연봉을 받으며 일한다는 것이다. 


그러면 임베디드 개발자도 양극화가 있을까? 내가 겪어 본 바로는 개발자 양극화도 심하다고 본다. 

내가 말하는 양극화는 기회의 박탈이다. 즉 실무 경험을 통해 개발 능력을 키울 기회를 의미한다. 


임베디드 개발자 등급도 소고기 등급과 같이 특급부터 3등급까지 여러 등급으로 나눌 수 있는데 언제 등급이 매겨지는지 살펴보자. 


가장 크게, 임베디드 개발자 등급은 취업할 때 입사하는 회사에 따라 나눠진다. 

여기서 말하는 회사는 연봉과 이름값만을 의미하지 않는다. 역량 있는 개발자로 성장할 수 있는 실무 프로젝트를 할 수 있는 회사다. 


아래 조건에서 임베디드 시스템을 개발을 시작하는 개발자는 우선 3~4등급으로 분류될 수밖에 없다. 왜냐? 

제대로 개발을 안 하니 실무능력이 쌓이지 않기 때문이다. 


1. 임베디드 장비로 디바이스 테스트만 하거나 시간을 많이 갈아 넣으면 할 수 있는 소모적인 일을 하는 개발 부서 

2. 코딩한 줄 하지 않고 다른 업체가 포팅한 드라이버를 올려 테스트를 하며 갑질을 하는 개발 부서 

3. 이미 안정화된 리눅스 커널에 드라이버 코드만 약간 수정하면서 프로젝트를 진행하는 개발 부서 

4. 남들이 해 놓은 드라이버를 컴파일이나 빌드를 빨리 해서 디바이스에 올리는 일을 하는 개발 부서 


회사의 이름 네임밸류가 높거나 연봉이 높다고 해도 이런 조건으로 일하는 임베디드 개발자의 역량을 인정해 줄까?


절~대 그렇지 않다. 


임베디드 개발자의 생명은 디버깅 능력이라 생각한다. 디버깅 능력은 문제 해결 능력과 직결되며 문제 해결 능력이 없는 개발자는 이 업계에서 인정해주지 않는다. 그런데 제대로 실무 프로젝트를 맡으며 만나는 여러 문제를 해결하는 과정이 필요한데 이런 **기회**를 박탈당한 개발자는 디버깅 기술 역량이 업그레이드되지 않는다. 


내가 여러 임베디드 모임이나 친구들과의 교류로 알게 된 사실은 많은 한국 임베디드 회사는 제대로 개발을 안 한다는 것이다. 

그 이유는 한국 임베디드 IT 업체들은 실리콘 밸리에 있는 개발자와 경쟁할 수 있는 원천 기술이 녹아 있는 소프트웨어를 개발하려는 의지가 보이지 않기 때문이다. 대부분 미국 등 해외 업체가 구현한 소프트웨어를 올려서 커스터마이즈하는 수준의 일을 하는 경우가 많다. 


그럼 이제 1차 관문에서 다른 임베디드 개발자 등급은 가려졌으니 이제 1등급부터 특급으로는 가는 길만 있지 않을까? 

하지만 현실은 그렇지 않다. 운 좋게 1차 관문을 통과해서 좋은 회사에 취업해도 3~4등급으로 떨어질 수 있다. 


제대로 실무 경험을 쌓을 수 있는 회사에 배치됐다고 해도 그 부서 배치나 그 부서의 개발자 육성 정책에 따라 개발자 등급이 또 나뉘기 때문이다. 


1. 회사의 운명을 결정 짓는 최신 임베디드 시스템을 개발하는 부서에 배치된 경우 

- 쟁쟁한 개발자 틈에서 맨날 테스트만 한다. 

- 핵심 디버깅 기법은 핵심 개발자들끼리 이야기하고 공유하지 않는다. 

- 맨날 이메일을 쓰면서 다른 업체 인터페이스 역할만 한다. 


2. 정치 싸움에 밀려 개별화만커스터마이즈만 하는 프로젝트를 하는 개발 부서에 배치 

- 빌드 머신이 되어 하루에 3~4개씩 이미지를 배포하고 테스트만 한다. 코드 분석할 시간은 물론 없다. 

- 커스터마이즈만 하는 일만 하는 부서이니 선배 개발자 개발 능력이 허접해 배울 것이 없다. 


이름 값하는 회사에서도 벌레처럼 기어다니듯 개발자도 수 없이 많다.


제대로 개발 능력을 키울 수 있는 프로젝트에 투입되는 것이 현실에서는 **정말** 쉽지 않다. 

1~2 관문을 통과하고 정말 실무 경험을 키울 수 있는 프로젝트에서 일하면 특급 개발자가 될 수 있나? 

그렇지 않다. 끊임없는 노력으로 학습을 해야 특급 개발자가 될 수 있는 것 같다.

리눅스 커널은 오픈 소소다. 모든 개발자나 학생들이 소스 코드를 들여다볼 수 있어 많은 정보를 알 수 있다. 함수 이름과 자료 구조를 보면서 리눅스 커널을 익힐 수 있다. 


난 운영체제를 소스 코드를 보면서 익힐 수 있는 리누즈 토발즈 및 오픈 소스 정신을 부르짖는 분들께 감사를 드리고 싶다. 리눅스 커널이 오픈 소스 프로젝트가 아니였다면 난 아마 RTOS 운영체제로 임베디드 디바이스 개발을 했거나 아예 다른 오픈 소스 프로젝트에 뛰어들었을 지도 모른다.

RTOS 회사가 알려주는 운영체제 개념도를 보면서 **그려러니** 하며 머릿속 개념으로 남아 있을 것이다.  


오픈 소스인 리눅스 커널로 누리는 혜택(?)이 있지만 만만치 않은 걸림돌이 있다.

리눅스 커널 소스 코드를 이해하면서 읽기가 너무 어렵다는 것이다. 구조체와 자료 구조가 너무 복잡하다. 


또한 리눅스 커널 소스에서 매크로를 정말 많이 쓴다. 그런데 매크로가 매크로를 5중으로 치환하면서 함수에서 쓰는 매크로 실제 값을 알기도 어렵다.


한 가지 예를 들어보자. 다음은 워크 멤버 중 data 필드로 워커 풀을 읽는 함수다.

static struct worker_pool *get_work_pool(struct work_struct *work)

{

unsigned long data = atomic_long_read(&work->data);

int pool_id;


assert_rcu_or_pool_mutex();


if (data & WORK_STRUCT_PWQ)

return ((struct pool_workqueue *)

(data & WORK_STRUCT_WQ_DATA_MASK))->pool;


pool_id = data >> WORK_OFFQ_POOL_SHIFT;

if (pool_id == WORK_OFFQ_POOL_NONE)

return NULL;


return idr_find(&worker_pool_idr, pool_id);

}


WORK_STRUCT_WQ_DATA_MASK이란 매크로 변수가 어떤 값인지 바로 이해할 수 있나?


다음 코드 선언부와 같이 WORK_STRUCT_FLAG_MASK 매크로를 알려면 WORK_STRUCT_FLAG_MASK, WORK_STRUCT_FLAG_BITS, WORK_STRUCT_COLOR_SHIFT, WORK_STRUCT_COLOR_BITS 매크로를 알아야 한다.

WORK_STRUCT_FLAG_MASK = (1UL << WORK_STRUCT_FLAG_BITS) - 1,

WORK_STRUCT_WQ_DATA_MASK = ~WORK_STRUCT_FLAG_MASK,

WORK_STRUCT_FLAG_BITS = WORK_STRUCT_COLOR_SHIFT +  WORK_STRUCT_COLOR_BITS,


WORK_STRUCT_COLOR_SHIFT 매크로를 분석하려고 하는데 어라? CONFIG_DEBUG_OBJECTS_WORK이란 컨피그에 따라  WORK_STRUCT_COLOR_SHIFT값이 달라진다. 

enum {

WORK_STRUCT_PENDING_BIT = 0, /* work item is pending execution */

WORK_STRUCT_DELAYED_BIT = 1, /* work item is delayed */

WORK_STRUCT_PWQ_BIT = 2, /* data points to pwq */

WORK_STRUCT_LINKED_BIT = 3, /* next work is linked to this one */

#ifdef CONFIG_DEBUG_OBJECTS_WORK

WORK_STRUCT_STATIC_BIT = 4, /* static initializer (debugobjects) */

WORK_STRUCT_COLOR_SHIFT = 5, /* color for workqueue flushing */

#else

WORK_STRUCT_COLOR_SHIFT = 4, /* color for workqueue flushing */

#endif 


최소 3~4년 리눅스 디바이스 드라이버를 만지면서 실무 개발에 뛰어든 개발자들을 제외하고 리눅스 커널을 처음 소스 코드로 공부한다는 것은 불가능하다. 


다른 이유는 뭘까? 

분석 중인 소스 코드가 실제 리눅스 시스템에서 어떻게 실행하는지 모르기 때문이다. 좀 유식한 말로 콜스택을 알 수 없기 때문이다.  

또한 분석 중인 코드가 컴파일되는 코드인지 모를 때도 있다. 

아예 컴파일 되지 않는 코드를 깊게 분석을 하는 가련한 개발자들을 보면 참 측은한 마음이 생긴다. 말 그대로 개삽질을 하고 있기 때문이다.


리눅스 커널을 익히기 어려운 또 다른 이유는 뭘까?

리눅스 커널을 쉽게 익힐 수 있는 툴이 부족하다. C# 프로그래밍을 하려면 C#용 Studio 프로그램을 설치하고 코딩을 하면 된다. 

그런데 리눅스 커널을 쉽게 설치하고 코딩할 수 있는 시스템이 있나? 사실 없다고 봐야 한다. 컴퓨터에 Ubuntu를 직접 설치하고 크로스 컴파일러 환경을 설정한 후 일일이 명령어를 입력해야 한다. 시스템에서 구동시키려면 엄청 손이 많이 간다. 


마지막으로 리눅스 커널을 쉽게 설명한 책이나 블로그도 없다. 

너무 이론 위주로 설명된 책이 대부분이라 신입 개발자들은 무슨 소리인지 알기 어렵고 블로그도 대부분 소스 코드 몇 줄 복사해 놓고 간단한 분석 내용을 요약해 놓은 수준에 불과하다. 


해외 신입 리눅스 개발자들은 한국과 상황이 약간 다르다. 해외 사이트의 경우 Stack Overflow나 영어로 된 리눅스 커널 입문자들을 위한 사이트가 많다. 영어만 제대로 해도 많은 정보를 얻을 수 있다.


리눅스 커널은 오픈 소스 프로젝트이지만 한국 신입 개발자들에겐 사실상 오픈 프로젝트가 아니다. 리눅스 커널 앞에 거대한 기술 장벽이 있다는 생각이 든다.

하지만 한국의 경우 이런 리눅스 커널을 익히기 위한 인프라가 절대적으로 열악하다.


4차 산업 혁명 시대가 열리면서 리눅스 디바이스 수요도 증가하고 있다. 수요 맞게 리눅스 커널 인프라를 한국에도 깔 필요가 있다. 

+ Recent posts