Overview

When debugging ramdump with stability issues, I have been spending most of time casting various data structure as belows.

v.v %s (struct task_struct*)0xC1917FCC

For this matter, I made several macros to minimize the debugging time.

The definition of the macro is below.

sYmbol.NEW.MACRO offsetof(type,member) ((int)(&((type*)0)->member))

sYmbol.NEW.MACRO container_of(ptr,type,member) ((type *)((char *)(ptr)-offsetof(type,member)))


offsetof(type,member)

When analyzing assembly code, I have to know the offset of certain element, assembly code is generated based upon offset value of data structure.

[1]: offset: struct kgsl_context.device is calculated as 0x1C

v.v % offsetof(struct kgsl_context,device)

  (int) offsetof(struct kgsl_context,device) = 28 = 0x1C = '....'

[assemble code]

NSR:C0500F68|E594501C                                  ldr     r5,[r4,#0x1C]

[2]: offset: struct adreno_context.rb

v.v % offsetof(struct adreno_context,rb)

  (int) offsetof(struct adreno_context,rb) = 768 = 0x0300 = '....'

[assemble code]

NSR:C0501054|E5943300                                  ldr     r3,[r4,#0x300]


Code segment for this example

449void adreno_drawctxt_detach(struct kgsl_context *context)

450{

451 struct kgsl_device *device;

452 struct adreno_device *adreno_dev;

453 struct adreno_context *drawctxt;

454 struct adreno_ringbuffer *rb;

455 int ret, count, i;

456 struct kgsl_cmdbatch *list[ADRENO_CONTEXT_CMDQUEUE_SIZE];

//snip

460

461 device = context->device;  // <<--[1]

462 adreno_dev = ADRENO_DEVICE(device);

463 drawctxt = ADRENO_CONTEXT(context);

464 rb = drawctxt->rb; // <<--[2]

465


container_of(ptr,type,member)

(Example1)

In order to find out the total element of "struct task_struct" as per "struct task_struct.tasks.next",

I have to manipulate the T32 many times. For this, let me introduce the container_of(ptr,type,member) macro

v.v %h container_of(0xEE458238,struct task_struct,tasks)

  container_of(0xEE458238,struct task_struct,tasks) = 0xEE458000 -> (

    state = 0x1,

    stack = 0xEE44A000,

//snip

    cputime_expires = (utime = 0x0, stime = 0x0, sum_exec_runtime = 0x0),

    cpu_timers = ((next = 0xEE458380, prev = 0xEE458380), (next = 0xEE458388, pr

    real_cred = 0xE2494D00,

    cred = 0xE2494D00,

    comm = "init",


(where)

  [D:0xC16141E8] init_task = (

    [D:0xC16141E8] state = 0x0,

    [D:0xC16141EC] stack = 0xC1600000,

//snip

    [D:0xC161441C] rcu_blocked_node = 0x0,

    [D:0xC1614420] tasks = (

      [D:0xC1614420] next = 0xEE458238,  // <<--

      [D:0xC1614424] prev = 0xC288B938),

    [D:0xC1614428] pushable_tasks = ([D:0xC1614428] prio = 0x8C, [D:0xC161442C] prio_list = ([D:0xC161442C] next = 0x

//snip

    [D:0xC1614584] cred = 0xC1619D18,

    [D:0xC1614588] comm = "swapper/0",

    [D:0xC1614598] link_count = 0x0,

    [D:0xC161459C] total_link_count = 0x0,


v.v %h %s container_of(0xDB63FE68,struct mutex,wait_list)

  container_of(0xDB63FE68,struct mutex,wait_list) = 0xDB63FE54 -> (

    count = (counter = 0xC0C0A56C),

    wait_lock = (rlock = (raw_lock = (slock = 0xC16A057C, tickets = (owner = 0x057C, next = 0xC16A)), magic = 0xC0C08C70, o

    wait_list = (next = 0xDAC43DB8, prev = 0xC15DBC40),

    owner = 0xE0396E00,

    name = 0xDB63FE68 -> ".=..@.].",

    magic = 0xFFFFFFFF)


  container_of(0xDAC43DB8,struct mutex,wait_list) = 0xDAC43DA4 -> (

    count = (counter = 0xC0C0A56C),

    wait_lock = (rlock = (raw_lock = (slock = 0xC16A057C, tickets = (owner = 0x0

    wait_list = (next = 0xDAC55E68, prev = 0xDB63FE68),

    owner = 0xDD5C5D80,

    name = 0xDAC43DB8 -> "h^..h.c..]\..=..``v.,.].",

    magic = 0xDB766060)


(where)  binder_main_lock = (

    count = (counter = 0xFFFFFFFF),

    wait_lock = (rlock = (raw_lock = (slock = 0xF1CCF1CC, tickets = (owner = 0xF

    wait_list = (

      next = 0xDB63FE68 // <<--

        next = 0xDAC43DB8 // <<--

          next = 0xDAC55E68 -> (

            next = 0xC15DBC40 -> (

              next = 0xDB63FE68,


container_of_double_vcast(ptr,type,member,new_member,cast_type)

Definition

sYmbol.NEW.MACRO container_double_vcast(ptr,type,member,new_member,cast_type) ((cast_type *)(*(type *)((char *)(ptr)-offsetof(type,member)+offsetof(type,new_member))))

(Example1: PHONEMODEL-1958)

container_of_double_vcast(0xDAC43DB8,struct mutex,wait_list,owner,struct task_struct)

  container_of_double_vcast(0xDAC43DB8,struct mutex,wait_list,owner,struct task_struct) = 0xDD5C5D80 -> (

    state = 0x2,

    stack = 0xDAC42000,

    usage = (counter = 0x2),

    flags = 0x00400040,

    ptrace = 0x0,

    wake_entry = (next = 0x0),

    on_cpu = 0x0,

    on_rq = 0x0,

    prio = 0x78,

    static_prio = 0x78,


(where)

  binder_main_lock = (  // <<-- type: struct mutex

    count = (counter = 0xFFFFFFFF),

    wait_lock = (rlock = (raw_lock = (slock = 0xF1CCF1CC, tickets = (owner = 0xF

    wait_list // <<-- member

      next = 0xDB63FE68

        next = 0xDAC43DB8 // <<--  ptr

          next = 0xDAC55E68 -> (

            next = 0xC15DBC40 -> (

              next = 0xDB63FE68,

              prev = 0xDAC55E68),

            prev = 0xDAC43DB8),

          prev = 0xDB63FE68),

        prev = 0xC15DBC40),

      prev = 0xDAC55E68),

    owner = 0xD9E6A100,  // <<-- new_member, cast_type: struct task_struct

    name = 0x0,

    magic = 0xC15DBC2C)


container_down_vcast(ptr,type,member,cast)

Definition

sYmbol.NEW.MACRO container_down_vcast(ptr,type,member,cast) ((cast *)(*(type *)((char *)(ptr)+offsetof(type,member))))


v.v % container_down_vcast(0xC15DBC2C,struct mutex,owner,struct task_struct)

v.v % container_down_vcast(0xC15DBC2C,struct mutex,owner,struct task_struct)

  container_down_vcast(0xC15DBC2C,struct mutex,owner,struct task_struct) = 0xD9E6A100 -> (

    state = 1,

    stack = 0xD9ECC000,

    usage = (counter = 2),

//snip

    min_flt = 304,

    maj_flt = 0,

    cputime_expires = (utime = 0, stime = 0, sum_exec_runtime = 0),

    cpu_timers = ((next = 0xD9E6A448, prev = 0xD9E6A448), (next = 0xD9E6A450, prev = 0xD9E6A450), (next = 0xD9E6A45

    real_cred = 0xDEEF1900,

    cred = 0xDEEF1900,

    comm = "Binder_4",

    link_count = 0,


(where)

  [D:0xC15DBC2C] binder_main_lock = (  // <<-- ptr: 0xC15DBC2C, type: struct mutex

    [D:0xC15DBC2C] count = ([D:0xC15DBC2C] counter = -1),

    [D:0xC15DBC30] wait_lock = ([D:0xC15DBC30] rlock = ([D:0xC15DBC30] raw_lock

    [D:0xC15DBC40] wait_list = ([D:0xC15DBC40] next = 0xDB63FE68, [D:0xC15DBC44]

    [D:0xC15DBC48] owner = 0xD9E6A100,  // <<--member, cast: struct task_struct

    [D:0xC15DBC4C] name = 0x0,

    [D:0xC15DBC50] magic = 0xC15DBC2C)


threadoffset(ptr)

Definition

sYmbol.NEW.MACRO threadoffset(ptr) ((ptr ~0x1fff))

sYmbol.NEW.MACRO thread_of(ptr) ((struct thread_info *)((int *)threadoffset(ptr)))

When kernel crash occurs, the kernel dumps below logs

[ 1894.897301] Internal error: Oops - BUG: 0 [#1] PREEMPT SMP ARM

[ 1894.897314] Modules linked in: texfat(PO)

[ 1894.897333] CPU: 2 PID: 4324 Comm: Binder_4 Tainted: P        W  O 3.10.49-g184f2e4 #1

[ 1894.897347] task: d9e6a100 ti: d9ecc000 task.ti: d9ecc000

[ 1894.897362] PC is at __list_add+0x9c/0xd0

[ 1894.897376] LR is at __list_add+0x58/0xd0

[ 1894.897390] pc : [<c032e9e8>]    lr : [<c032e9a4>]    psr: 000f0093

[ 1894.897390] sp : d9ecdd90  ip : 00000000  fp : dc08da00

[ 1894.897409] r10: d9ecc000  r9 : c16a39ec  r8 : d9e6a100

[ 1894.897422] r7 : 00000000  r6 : d9ecddb8  r5 : c15dbc40  r4 : c0004860

[ 1894.897435] r3 : 00000000  r2 : 00001201  r1 : c16a28a8  r0 : 00000000

[ 1894.897450] Flags: nzcv  IRQs off  FIQs on  Mode SVC_32  ISA ARM  Segment user

[ 1894.897464] Control: 10c0383d  Table: 9e06006a  DAC: 00000015

[ 1894.897476] Process Binder_4 (pid: 4324, stack limit = 0xd9ecc238)

[ 1894.897489] Stack: (0xd9ecdd90 to 0xd9ece000)

[ 1894.897504] dd80:                                     00000000 c0004860 c15dbc40 c15dbc2c

[ 1894.897522] dda0: 600f0013 d9ecc030 c15dbc30 c0c08bcc d9ecdddc c15dbc40 d9ecddb8 d9ecddb8

[ 1894.897540] ddc0: 11111111 d9ecddb8 600f0013 c15dbc2c de364e00 b786a844 d9ecc038 c1660598

[ 1894.897557] dde0: 00000000 d9ecc000 dc08da00 c0c08e70 ddf0f000 c072d020 ded48000 c018b544

[ 1894.897574] de00: 00000000 800f0193 00000028 dd041600 c8002ab0 b781ee04 00000000 db0d1c00

[ 1894.897591] de20: c8002ad0 00000000 00000000 b786a840 b786a940 ddf0f01c d9ecdee0 d9ecc000


With any stack address, the (struct thread_info*) can be casted with the single command.

v.v %all thread_of(0xd9ecdd90)

  (struct thread_info *) thread_of(0xd9ecdd90) = 0xD9ECC000 = __bss_stop+0x1855F

    (long unsigned int) flags = 0 = 0x0 = '....',

    (int) preempt_count = 3 = 0x3 = '....',

    (mm_segment_t) addr_limit = 3204448256 = 0xBF000000 = '....',

    (struct task_struct *) task = 0xD9E6A100 = __bss_stop+0x184FD964 -> ((long i

    (struct exec_domain *) exec_domain = 0xC1579CDC = default_exec_domain -> ((c

    (__u32) cpu = 2 = 0x2 = '....',

    (__u32) cpu_domain = 21 = 0x15 = '....',

    (struct cpu_context_save) cpu_context = ((__u32) r4 = 3740230976 = 0xDEEF654

    (__u32) syscall = 0 = 0x0 = '....',

    (__u8 [16]) used_cp = "",

    (long unsigned int [2]) tp_value = ([0] = 3001371000 = 0xB2E54978 = '..Ix',

    (union fp_state) fpstate = ((struct fp_hard_struct) hard = ((unsigned int [3

    (union vfp_state) vfpstate = ((struct vfp_hard_struct) hard = ((__u64 [32])

    (struct restart_block) restart_block = ((long int (*)()) fn = 0xC0131A64 = d


v.v %all thread_of(0xd9ecddc0)

  (struct thread_info *) thread_of(0xd9ecddc0) = 0xD9ECC000 = __bss_stop+0x1855F

    (long unsigned int) flags = 0 = 0x0 = '....',

    (int) preempt_count = 3 = 0x3 = '....',

    (mm_segment_t) addr_limit = 3204448256 = 0xBF000000 = '....',

    (struct task_struct *) task = 0xD9E6A100 = __bss_stop+0x184FD964 -> ((long i

    (struct exec_domain *) exec_domain = 0xC1579CDC = default_exec_domain -> ((c

    (__u32) cpu = 2 = 0x2 = '....',

    (__u32) cpu_domain = 21 = 0x15 = '....',

    (struct cpu_context_save) cpu_context = ((__u32) r4 = 3740230976 = 0xDEEF654

    (__u32) syscall = 0 = 0x0 = '....',

    (__u8 [16]) used_cp = "",

    (long unsigned int [2]) tp_value = ([0] = 3001371000 = 0xB2E54978 = '..Ix',

    (union fp_state) fpstate = ((struct fp_hard_struct) hard = ((unsigned int [3

    (union vfp_state) vfpstate = ((struct vfp_hard_struct) hard = ((__u64 [32])

    (struct restart_block) restart_block = ((long int (*)()) fn = 0xC0131A64 = d


Now, let me look into the spinlock implmentation in more details.

After analyzing the assembly code, R2 is key debugging signature which is the original value of spinlock owner.


Code Review: arch_spin_lock(part 1) -> If the spinlock is acquired sucessfully.

Let me assume tickets.next=0x0, tickets.owner=0x0(spinlock is not held) before _raw_spin_lock() is executed

[1]: R2: (raw_spinlock_t *)lock is loaded from R0.

[2]: +1 increament struct raw_spinlock_t.raw_lock.tickets.next of R2 and save it into R3(0x1, raw_spinlock_t.raw_lock.tickets.next+1)

[3]: Save the incremented +0x1 next element into lock value

If "strex" insruction is executed successfully, r1=0x0, otherwise, r1=0x1

[4]: if R1 is 0x0, jump to 0xc0ee8c60, else go to [1]0xc0ee8c4c.

[5]: R3(0x0) is holding struct raw_spinlock_t.raw_lock.tickets.owner from R2(original spinlock)

Please be reminded that R2 holds original struct raw_spinlock_t* lock;

[6]: R2(0x0) contains struct raw_spinlock_t.raw_lock.tickets.next from R2

[7]: Compare R2(tickets.next) and R3(tickets.owner)

[8]: In this case, tickets.next == tickets.owner(spinlock is not held before this funtion is called), jump to 0xc0ee8c80 for function termination.

0xc0ee8c30 <_raw_spin_lock>:    mov     r2, sp

0xc0ee8c34 <_raw_spin_lock+0x4>:        bic     r3, r2, #8128   ; 0x1fc0

0xc0ee8c38 <_raw_spin_lock+0x8>:        bic     r3, r3, #63     ; 0x3f

0xc0ee8c3c <_raw_spin_lock+0xc>:        ldr     r2, [r3, #4]

0xc0ee8c40 <_raw_spin_lock+0x10>:       add     r2, r2, #1

0xc0ee8c44 <_raw_spin_lock+0x14>:       str     r2, [r3, #4]

0xc0ee8c48 <_raw_spin_lock+0x18>:       pldw    [r0]

0xc0ee8c4c <_raw_spin_lock+0x1c>:       ldrex   r2, [r0]  //<<--[1]

0xc0ee8c50 <_raw_spin_lock+0x20>:       add     r3, r2, #65536  ; 0x10000  //<<--[2]

0xc0ee8c54 <_raw_spin_lock+0x24>:       strex   r1, r3, [r0] //<<--[3]

0xc0ee8c58 <_raw_spin_lock+0x28>:       teq     r1, #0 //<<--[4]

0xc0ee8c5c <_raw_spin_lock+0x2c>:       bne     0xc0ee8c4c <_raw_spin_lock+28>

0xc0ee8c60 <_raw_spin_lock+0x30>:       uxth    r3, r2  //<<--[5]

0xc0ee8c64 <_raw_spin_lock+0x34>:       ubfx    r2, r2, #16, #16  //<<--[6]

0xc0ee8c68 <_raw_spin_lock+0x38>:       cmp     r2, r3  //<<--[7]

0xc0ee8c6c <_raw_spin_lock+0x3c>:       beq     0xc0ee8c80 <_raw_spin_lock+80> //<<--[8]

0xc0ee8c70 <_raw_spin_lock+0x40>:       wfe

0xc0ee8c74 <_raw_spin_lock+0x44>:       ldrh    r3, [r0]

0xc0ee8c78 <_raw_spin_lock+0x48>:       uxth    r3, r3

0xc0ee8c7c <_raw_spin_lock+0x4c>:       b       0xc0ee8c68 <_raw_spin_lock+56>

0xc0ee8c80 <_raw_spin_lock+0x50>:       dmb     ish

0xc0ee8c84 <_raw_spin_lock+0x54>:       bx      lr


Code Review: arch_spin_lock(part 2) -> If the spinlock is held by someone, so it waits for it to released.

Let me assume tickets.next=0x1, tickets.owner=0x0(the spinlock is already held) before _raw_spin_lock() is executed.

[1]: R2: (raw_spinlock_t *)lock is loaded from R0.

[2]: +1 increament struct raw_spinlock_t.raw_lock.tickets.next of R2 and save it into R3(0x2, raw_spinlock_t.raw_lock.tickets.next+1)

[3]: Save the incremented +0x1 next element into lock value(raw_spinlock_t.raw_lock.tickets.next is updated as 0x2)

If "strex" insruction is executed successfully, r1=0x0, otherwise, r1=0x1

[4]: if R1 is 0x0, jump to 0xc0ee8c60, else go to [1]0xc0ee8c4c.

[5]: R3(0x0) is holding struct raw_spinlock_t.raw_lock.tickets.owner from R2

Please be reminded that R2 holds original struct raw_spinlock_t* lock;

[6]: R2(0x1) contains struct raw_spinlock_t.raw_lock.tickets.next from R2

[7]: Compare R2(tickets.next: 0x1) and R3(tickets.owner: 0x0)

[8]: The tickets.next > tickets.owner means spinlock is already held, this code is executed.

R3 is loaded from (raw_spinlock_t *) lock which is the spinlock **instance**(which can be accessed other processes)

[9]: R3 is updated as struct raw_spinlock_t.raw_lock.tickets.owner. And then jump to [7] 0xc0ee8c68.

0xc0ee8c30 <_raw_spin_lock>:    mov     r2, sp

0xc0ee8c34 <_raw_spin_lock+0x4>:        bic     r3, r2, #8128   ; 0x1fc0

0xc0ee8c38 <_raw_spin_lock+0x8>:        bic     r3, r3, #63     ; 0x3f

0xc0ee8c3c <_raw_spin_lock+0xc>:        ldr     r2, [r3, #4]

0xc0ee8c40 <_raw_spin_lock+0x10>:       add     r2, r2, #1

0xc0ee8c44 <_raw_spin_lock+0x14>:       str     r2, [r3, #4]

0xc0ee8c48 <_raw_spin_lock+0x18>:       pldw    [r0]

0xc0ee8c4c <_raw_spin_lock+0x1c>:       ldrex   r2, [r0]  //<<--[1]

0xc0ee8c50 <_raw_spin_lock+0x20>:       add     r3, r2, #65536  ; 0x10000  //<<--[2]

0xc0ee8c54 <_raw_spin_lock+0x24>:       strex   r1, r3, [r0] //<<--[3]

0xc0ee8c58 <_raw_spin_lock+0x28>:       teq     r1, #0 //<<--[4]

0xc0ee8c5c <_raw_spin_lock+0x2c>:       bne     0xc0ee8c4c <_raw_spin_lock+28>

0xc0ee8c60 <_raw_spin_lock+0x30>:       uxth    r3, r2  //<<--[5]

0xc0ee8c64 <_raw_spin_lock+0x34>:       ubfx    r2, r2, #16, #16  //<<--[6]

0xc0ee8c68 <_raw_spin_lock+0x38>:       cmp     r2, r3  //<<--[7]

0xc0ee8c6c <_raw_spin_lock+0x3c>:       beq     0xc0ee8c80 <_raw_spin_lock+80>

0xc0ee8c70 <_raw_spin_lock+0x40>:       wfe

0xc0ee8c74 <_raw_spin_lock+0x44>:       ldrh    r3, [r0] //<<--[8]

0xc0ee8c78 <_raw_spin_lock+0x48>:       uxth    r3, r3  //<<--[9]

0xc0ee8c7c <_raw_spin_lock+0x4c>:       b       0xc0ee8c68 <_raw_spin_lock+56>

0xc0ee8c80 <_raw_spin_lock+0x50>:       dmb     ish

0xc0ee8c84 <_raw_spin_lock+0x54>:       bx      lr


Code Review: arch_spin_lock(part 2.1): if process is waiting for spinlock to be released.

Running [7]-[8]-[9] loop until struct raw_spinlock_t.raw_lock.tickets.owner is increated 0x1(which means spinlock is released)

0xc0ee8c68 <_raw_spin_lock+0x38>:       cmp     r2, r3  //<<--[7]

0xc0ee8c6c <_raw_spin_lock+0x3c>:       beq     0xc0ee8c80 <_raw_spin_lock+80>

0xc0ee8c70 <_raw_spin_lock+0x40>:       wfe

0xc0ee8c74 <_raw_spin_lock+0x44>:       ldrh    r3, [r0] //<<--[8]

0xc0ee8c78 <_raw_spin_lock+0x48>:       uxth    r3, r3  //<<--[9]

Please be reminded that R2 holds original struct raw_spinlock_t* lock.ticker.owner;


Code Review: arch_spin_lock(part 2.2): Exit this function after the spinlock is released.


After running [7]-[8]-[9] loop...

[8]: R3 is loaded from (raw_spinlock_t *) lock which is the spinlock **instance**(which can be accessed other processes)

[9]: At this point, if other process releases spinlock, R3 is updated as 0x1 (as struct raw_spinlock_t.raw_lock.tickets.owner is increamented).

[7]: Since R2 == R3(spinlock is released), exit this function.

0xc0ee8c68 <_raw_spin_lock+0x38>:       cmp     r2, r3  //<<--[7]

0xc0ee8c6c <_raw_spin_lock+0x3c>:       beq     0xc0ee8c80 <_raw_spin_lock+80>

0xc0ee8c70 <_raw_spin_lock+0x40>:       wfe

0xc0ee8c74 <_raw_spin_lock+0x44>:       ldrh    r3, [r0] //<<--[8]

0xc0ee8c78 <_raw_spin_lock+0x48>:       uxth    r3, r3  //<<--[9]


To wrap up the spinlock operation in easy way, let me picture the following scenario.

1. Scenario: next: 0x0, owner:0x0(before spinlock is called)

[1]. Increament next +1(next: 0x1)

[2]: Exit spinlock

2. Scenario: next: 0x1, owner:0x0(before spinlock is called)

[1]. Increament next +1(next: 0x2)

[2]: Original next(0x1) is saved R2

[3]: Loop until the owner of spinlock instance is updated as 0x1(by other process)

[4]: Exit spinlock(next: 0x2, owner:0x1)

3. Scenario: next: 0x45, owner:0x41(before spinlock is called): it means the spinlock is held by 4 times.

[1]. Increament next +1(next: 0x46)

[2]: Original next(0x45) is saved R2

[3]: Loop until the owner of spinlock instance is updated as 0x45(by other process)

[4]: Exit spinlock(next: 0x46, owner:0x45)


스케줄링(scheduling)/스케줄러(schedule)란 무엇일까? 


여러분들은 리눅스 시스템이 탑재된 휴대폰이나 라즈베리파이를 쓰면 동시에 여러 프로그램을 실행할 수 있습니다. 휴대폰을 보면 다양한 프로그램이 동시에 실행하는 것을 확인할 수 있습니다. 예를 들면 브라우저를 실행하면서 음악을 듣거나 메신저를 하면서 어플리케이션을 다운로드 할 수 있습니다.

그래서 사람들은 여러 프로세스들이 동시에 CPU에서 실행한다고 느낄 수 있습니다. 하지만 CPU는 여러 개의 프로세스를 절대로 동시에 실행할 수는 없습니다. 리눅스 커널을 포함한 다양한 운영체제에서 스케줄링과 멀티 태스킹 기법이 생겨난 이유는 다음과 같습니다.
CPU는 한 순간에 한 개의 프로세스의 코드만을 실행할 수 있습니다.

여러 개의 프로세스들이 효율적으로 번갈아 CPU에서 실행할 수 있게 규칙을 부여하고 프로세스들을 관리하는 소프트웨어 모듈을 스케줄러라고 말합니다.

하나의 프로세스는 CPU에서 실행을 시작하면 계속 CPU에서 실행하는 것이 아니라 실행을 하다가 잠깐 멈추고 다시 실행하는 방식으로 동작합니다. 즉, 프로세스는 CPU를 점유하면서 실행 중인 상태와 실행 대기하는 상태로 계속 변경하는 것입니다.

메모리에 존재하는 여러 프로세스 중에서 실제 CPU에서 실행될 프로세스를 선택하는 일을 스케줄링이라고 말합니다. 이 때 어떤 프로세스를 어떤 방식으로 선택할지를 결정해야 합니다. 

스케줄링 동작은 다음 그림으로 표현할 수 있습니다.
 

CPU에서 실행하려고 대기 중인 Process A ~ Process D 프로세스 중 하나를 선택해서 CPU에서 실행시키는 동작입니다.

스케줄링 동작을 다른 각도에서 살펴보겠습니다. 다음 그림은 프로세스 상태 변화 다이어그램입니다.
 

커널은 프로세스에게 프로세스 상태를 부여합니다. 프로세스가 생성 및 실행된 후 종료할 때까지 위와 같은 상태 변화로 동작합니다.

프로세스가 CPU에서 실행하기 위해서는 실행 대기(TASK_RUNNING) 상태로 변경한 다음 커널 스케줄링에 의해 CPU 실행(TASK_RUNNING) 상태로 변경되어야 합니다.

대부분 보통 프로세스 실행 상태 변화 흐름을 프로세스 1인칭으로 바라볼 때가 많습니다.
이번에는 시스템 전체 관점으로 프로세스 상태 변화 다이어그램을 살펴봅시다.

 

커널에서 실행 중인 전체 프로세스가 각각 어떤 상태로 실행 중인지를 보여주는 그림입니다. 원형으로 표시된 A~N는 각각 프로세스를 의미합니다.

A~D 프로세스들은 실행 대기(TASK_RUNNING) 상태에 있습니다. CPU에서 실행하기 위해 대기 중인 프로세스입니다.

CPU 실행(TASK_RUNNING) 상태를 보면 E 프로세스가 CPU에서 실행 중입니다.

다른 관점으로 스케줄링 동작을 다음과 같이 설명할 수 있습니다.
실행 대기(TASK_RUNNING) 상태에 있는 프로세스 중 하나를 선택해서 CPU 실행(TASK_RUNNING) 상태로 바꿔주는 동작

커널 스케줄링은 프로세스 상태 기준으로 실행 대기 중에 있는 프로세스를 어떤 방식으로 실행할지를 결정합니다. 따라서 프로세스 상태 정보는 매우 중요합니다.

대부분 드라이버 코드를 작성할 때 프로세스 상태를 변경하는 코드를 작성할 필요는 없습니다. 하지만, 커널 스레드 핸들 함수를 구현할 때 프로세스 상태를 변경하는 코드를 입력할 때가 있습니다. 이 때 반드시 set_current_state() 함수를 써서 프로세스 상태를 변경해야 합니다.

그 이유는 다음과 같이 set_current_state() 함수 코드를 보면 알 수 있습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/sched.h]
1 #define set_current_state(state_value) \
2 do { \
3 WARN_ON_ONCE(is_special_task_state(state_value));\
4 current->task_state_change = _THIS_IP_; \
5 smp_store_mb(current->state, (state_value)); \
6 } while (0)

5 번째 줄을 보면 smp_store_mb() 함수를 호출해서 메모리 배리어를 실행합니다.
메모리 배리어 코드를 추가하면 GCC 컴파일러가 코드 최적화를 위해 코드 위치를 변경하는 것을 방지합니다.


tst 명령어를 실행하면 ARM CPSR 레지스터가 어떻게 변경될까?


tst 명령어는 연산자와 비연산자 사이 AND 비트 연산을 수행합니다.


AND 비트 연산 결과에 따라 CPSR 레지스터 Z 비트는 다음과 같이 변경됩니다.

Z: 0 ( AND 비트 연산 결과가 1인 경우)

Z: 1 ( AND 비트 연산 결과가 0인 경우)


tst 명령어가 위와 같이 동작하는지 증명하기 위해 T32 디버거를 실행해 보겠습니다.

다음 화면은 tst 명령어를 실행하기 직전입니다. 

NSR:80107E6C|ret_to_user_from_irq:        ldr     r2,[r9,#0x8]

NSR:80107E70|                             cmp     r2,#0x7F000000   ; r2,#2130706432

NSR:80107E74|                             blne    0x8010B5FC       ; addr_limit_check_failed

NSR:80107E78|                             ldr     r1,[r9]

NSR:80107E7C|_____________________________tst_____r1,#0x0F_________;_r1,#15

NSR:80107E80|                             bne     0x80107E48       ; slow_work_pending

NSR:80107E84|no_work_pending:             bl      0x801E9CE8       ; trace_hardirqs_on


N _  R0          0  R8          0   

Z Z  R1          2  R9          0

C _  R2          0  R10         0

V _  R3          0  R11         0

Q _  R4   00020000  R12         0

     R5          0  R13  C000D000

0 _  R6          0  R14         0

1 _  R7          0  PC   80107E7C

2 _  SPSR       10  CPSR 400001D3


R1가 2(이진수 10)이니 0xF와 AND 비트 연산을 하면 1이 될 것입니다.

1111 (0xF)

  10 (2: R1) 

------

   1 (연산 결과)


다음은 "tst r1,#0x0F" 명령어를 실행한 후 결과입니다.

NSR:80107E6C|ret_to_user_from_irq:        ldr     r2,[r9,#0x8]

NSR:80107E70|                             cmp     r2,#0x7F000000   ; r2,#2130706432

NSR:80107E74|                             blne    0x8010B5FC       ; addr_limit_check_failed

NSR:80107E78|                             ldr     r1,[r9]

NSR:80107E7C|                             tst     r1,#0x0F         ; r1,#15

NSR:80107E80|_____________________________bne_____0x80107E48_______;_slow_work_pending

NSR:80107E84|no_work_pending:             bl      0x801E9CE8       ; trace_hardirqs_on


N _  R0          0  R8          0   

Z _  R1          2  R9          0

C _  R2          0  R10         0

V _  R3          0  R11         0

Q _  R4   00020000  R12         0

     R5          0  R13  C000D000

0 _  R6          0  R14         0

1 _  R7          0  PC   80107E80

2 _  SPSR       10  CPSR     01D3


여기서 눈을 크게 뜨고 봐야 할 중요한 정보는 ARM CPSR 레지스터입니다.

CPSR 레지스터가 0x1D3으로 변경됐습니다. CPSR 레지스터 Z 비트가 0이란 이야기입니다.


bne 명령어는 'branch not equal'이란 뜻입니다. Z가 0이면 브랜치하겠다는 의미입니다.

bne 0x80107E48 ;slow_work_pending


CPSR 레지스터 Z 비트가 0이니 slow_work_pending 레이블로 브랜치 할 것입니다.

NSR:80107E80|_____________________________bne_____0x80107E48_______;_slow_work_pending


이 결과를 예상하면서 위 "bne 0x80107E48 ;slow_work_pending" 코드를 실행하니 

slow_work_pending 레이블로 브랜치했습니다.

NSR:80107E48|E1A0000D__slow_work_pending:__cpy_____r0,r13

NSR:80107E4C|E1A02008                      cpy     r2,r8

NSR:80107E50|EB000D82                      bl      0x8010B460       ; do_work_pending

NSR:80107E54|E3500000                      cmp     r0,#0x0          ; r0,#0

NSR:80107E58|0A000009                      beq     0x80107E84       ; no_work_pending

NSR:80107E5C|B3A07000                      movlt   r7,#0x0          ; r7,#0

NSR:80107E60|E89D007F                      ldm     r13,{r0-r6}

NSR:80107E64|EA000032                      b       0x80107F34       ; local_restart


이번엔 r1 레지스터가 0x1000 일 때 tst 명령어가 어떤 동작을 하는지 확인해봅시다.

NSR:80107E6C|ret_to_user_from_irq:        ldr     r2,[r9,#0x8]

NSR:80107E70|                             cmp     r2,#0x7F000000   ; r2,#2130706432

NSR:80107E74|                             blne    0x8010B5FC       ; addr_limit_check_failed

NSR:80107E78|                             ldr     r1,[r9]

NSR:80107E7C|_____________________________tst_____r1,#0x0F_________;_r1,#15

NSR:80107E80|                             bne     0x80107E48       ; slow_work_pending

NSR:80107E84|no_work_pending:             bl      0x801E9CE8       ; trace_hardirqs_on


N _  R0          0  R8          0   

Z _  R1       1000  R9          0

C _  R2          0  R10         0

V _  R3          0  R11         0

Q _  R4   00020000  R12         0

     R5          0  R13  C000D000

0 _  R6          0  R14         0

1 _  R7          0  PC   80107E7C

2 _  SPSR       10  CPSR     01D3


위 명령어를 실행하기 전 CPSR 레지스터는 0x1D3이고 R1이 0x1000이란 사실을 기억합시다.

0xF는 이진수로 1111이고 R1가 저장하고 있는 0x1000은 이진수로 1111_0000_0000_0000 일 것입니다.


따라서 AND 비트 연산 결과는 0일 것입니다.

1111_0000_0000_0000  ( 0x1000: R1)

               1111  ( 0xF)

--------------------------------------------- AND 연산

            0    (결과)


이 사실을 염두해두고 "tst r1,#0x0F" 명령어를 T32로 실행하겠습니다.

결과 화면은 다음과 같습니다.


NSR:80107E6C|ret_to_user_from_irq:        ldr     r2,[r9,#0x8]

NSR:80107E70|                             cmp     r2,#0x7F000000   ; r2,#2130706432

NSR:80107E74|                             blne    0x8010B5FC       ; addr_limit_check_failed

NSR:80107E78|                             ldr     r1,[r9]

NSR:80107E7C|                             tst     r1,#0x0F         ; r1,#15

NSR:80107E80|_____________________________bne_____0x80107E48_______;_slow_work_pending

NSR:80107E84|no_work_pending:             bl      0x801E9CE8       ; trace_hardirqs_on


[레지스터 세트]

N _  R0          0  R8          0   

Z Z  R1       1000  R9          0

C _  R2          0  R10         0

V _  R3          0  R11         0

Q _  R4   00020000  R12         0

     R5          0  R13  C000D000

0 _  R6          0  R14         0

1 _  R7          0  PC   80107E80

2 _  SPSR       10  CPSR 400001D3


CPSR 레지스터가 400001D3 이고 Z 비트가 1이 됐습니다.

레지스터 세트에서 붉은색으로 표시된 부분을 눈으로 따라가 보시기 바랍니다.

N _  R0          0  R8          0   

Z Z  R1       1000  R9          0


Z 필드가 켜져 있습니다.


Z 비트가 1인데 다음 bne 명령어를 실행하면 어떻게 실행할까요? 

NSR:80107E80|_____________________________bne_____0x80107E48_______;_slow_work_pending

NSR:80107E84|no_work_pending:             bl      0x801E9CE8       ; trace_hardirqs_on


당연히 slow_work_pending 레이블로 브랜치하지 않고 80107E84 주소로 브랜치할 것입니다.

NSR:80107E6C|ret_to_user_from_irq:        ldr     r2,[r9,#0x8]

NSR:80107E70|                             cmp     r2,#0x7F000000   ; r2,#2130706432

NSR:80107E74|                             blne    0x8010B5FC       ; addr_limit_check_failed

NSR:80107E78|                             ldr     r1,[r9]

NSR:80107E7C|                             tst     r1,#0x0F         ; r1,#15

NSR:80107E80|                             bne     0x80107E48       ; slow_work_pending

NSR:80107E84|no_work_pending:_____________bl______0x801E9CE8_______;_trace_hardirqs_on

- 시스템 콜은 누가 언제 실행할까요?


시스템 콜은 유저 모드에서 실행 중인 어플리케이션에서 커널에게 어떤 서비스를 요청할 때 실행합니다. 유저 어플리케이션에서 파일 시스템에 접근해서 파일을 읽고 쓰거나 PID와 같은 프로세스 정보를 얻으려 할 때 주어진 규약에 맞게 커널에 서비스를 요청을 하는 것입니다. 이를 위해 시스템 콜을 발생해서 유저 공간에서 커널 공간으로 실행 흐름을 이동합니다.


이 동작은 다음 그림으로 표현할 수 있습니다.



이번에 시스템 콜 세부 동작을 왜 잘 알아야 하는지 생각해봅시다.

시스템 콜은 리눅스 시스템에서 당연히 잘 동작하는데 왜 알아야 할까요? 그 이유는 문제 해결 능력을 키우기 위해서입니다. 리눅스 시스템 저수준 함수를 써서 응용 어플리케이션 코드는 누구나 작성할 수 있습니다. 하지만 시스템 콜이 유저 공간에서 커널 공간까지 어떤 흐름으로 동작하는지 모르면 어디부터 문제 원인을 분석해야 할지 알 수 없습니다.


 시스템 콜이 어떤 흐름으로 동작하는지 잘 모르고 매뉴얼에 있는 내용만 참고해서 코드 작성하는 분보다 시스템 콜 전체 흐름을 제대로 이해한 분이 더 안정적인 코드를 작성할 가능성이 높습니다. 특정 리눅스 시스템 함수를 호출했는데 갑자기 에러 코드를 음수로 반환한다고 가정합시다. 시스템 인터페이스 구조를 알면 어느 코드부터 분석을 시작할지 판단할 수 있습니다.


리눅스에서는 실행 공간을 메모리 접근과 실행 권한에 따라 유저 공간과 커널 공간으로 분류합니다. 


먼저 커널 공간이 무엇인지 알아봅시다. 

커널 코드가 실행할 때는 모든 커널 함수 호출이 가능하며 제약 없이 메모리 공간에 접근해서 하드웨어를 제어할 수 있습니다. 이런 시스템 상태와 메모리 접근을 커널 공간이라고 부릅니다. 


다음은 유저 공간을 소개하겠습니다. 유저 어플리케이션 코드가 구동하는 동작과 상태를 유저 공간이라고 합니다. 유저 어플리케이션은 유저 공간에서 실행하며 메모리 공간 접근에 제한이 있고 하드웨어에 직접 접근할 수 없습니다. 


     

유저 어플리케이션에서 권한이 없는 메모리 공간에 접근하면 커널은 오류를 감지해서 해당 프로세스를 종료시킵니다.



다음 소절에 이어 시스템 콜 전체 흐름도와 동작에 대해서 살펴보겠습니다.


- 시스템 콜(시스템 호출)은 왜 필요할까?


시스템 콜은 유저 모드에서 커널 모드로 진입하는 동작입니다. 다른 관점으로 시스템 콜은 유저 공간과 커널 공간 사이 가상 계층이자 인터페이스라고 볼 수도 있습니다. 이 계층은 다음과 같은 특징이 있습니다.

1. 시스템 안정성과 보안을 지킬 수 있습니다. 유저모드에서 어플리케이션이 커널 공간에 아무런 제약없이 접근한다고 가정합시다. 실수로 어플리케이션이 커널 코드 영역 메모리를 오염을 시키면 시스템은 오동작할 가능성이 높습니다.


2. 유저 어플리케이션에서 추상화된 하드웨어 인터페이스를 제공합니다. 유저 모드에서 구동 중안 어플리케이션 입장에서 하나의 파일 시스템 위에서 구동 중인 것으로 착각하게 합니다.


3. 시스템 콜 구현으로 유저 어플리케이션의 호환성과 이식성을 보장할 수 있습니다. 리눅스 시스템은 시스템 콜 인터페이스는 POSIX(Portable Operating System Interface) 이란 유닉스 표준 규약에 맞게 구현되어 있기 때문입니다. 이로 유저 어플리케이션 코드를 라즈베리파이, 안드로이드 등 리눅스 계열의 시스템과 유닉스 운영체제에서도 구동할 수 있습니다.


4. 유저 공간에서 실행하는 어플리케이션에서 커널 공간으로 진입하는 인터페이스를 두고 커널과 독립적으로 구동합니다. 유저 어플리케이션 입장에서 파일 시스템과 프로세스 생성과 같은 내부 동작에 신경 쓸 필요가 없습니다.


리눅스 디바이스 드라이버와 가상 파일 시스템 함수도 시스템 콜을 통해 시스템 콜 핸들러를 통해 관련 코드를 실행합니다.  


또한 시스템 콜은 ARM 아키텍처와 연관이 깊은 동작입니다. ARM 프로세서는 시스템 콜을 익셉션의 한 종류인 소프트웨어 인터럽트로 실행하기 때문입니다. ARM 프로세스 관점으로 시스템 콜을 어떻게 처리하는지 알아볼 필요가 있습니다.


- 시스템 콜 전체 흐름도 소개


이전에 소개한 시스템 콜 흐름도와 시스템 콜 동작은 그리 간단하지 않습니다. 시스템 콜 세부 동작을 알려면 다음 시스템 전체 흐름도를 이해해야 합니다.


다음 그림은 이번에 다룰 전체 시스템 콜 흐름도입니다.




open(), write() 그리고 read() 함수는 파일을 열고 읽어서 쓰는 파일 입출력 동작이고, fork()와 exit() 함수는 프로세스 생성과 종료와 연관된 동작을 실행합니다. 이를 리눅스 저수준 함수라고 부릅니다. 다른 관점으로 GNU C 라이브러리로 진입하는 함수이며 이를 API(Application Programming Interface) 라고 부릅니다.


리눅스 시스템에서는 390여 개의 표준 함수들이 있는데 위 그림에서 대표적인 함수 5개를 표현한 것입니다.


라즈베리파이에서 다음 파일을 열어보면 시스템 콜 번호를 확인할 수 있습니다.

[/usr/include/arm-linux-gnueabihf/asm/unistd.h]

#define __NR_restart_syscall (__NR_SYSCALL_BASE+  0)

#define __NR_exit (__NR_SYSCALL_BASE+  1)

#define __NR_fork (__NR_SYSCALL_BASE+  2)

...

#define __NR_pkey_mprotect (__NR_SYSCALL_BASE+394)

#define __NR_pkey_alloc (__NR_SYSCALL_BASE+395)

#define __NR_pkey_free (__NR_SYSCALL_BASE+396)


시스템 콜을 제대로 이해하려면 시스템 콜을 발생하는 유저 공간부터 시스템 콜을 실행하는 커널 공간 계층까지 전체 흐름도를 살펴볼 필요가 있습니다.


시스템 콜 실행 흐름은 4단계로 나눌 수 있습니다.


1 단계: 리눅스 저수준 표준 함수 호출

유저 어플리케이션에서 파일시스템에 접근해서 파일을 열고 읽고 쓰려고 할 때 open(), write(), read() 함수를 호출해야 합니다. 혹은 프로세스를 생성하거나 종료할 때 fork() 나 exit() 함수를 호출합니다. 이 함수들은 API(Application Programming Interface)라고 말합니다. 유저 어플리케이션에서 리눅스 커널에서 제공하는 기능을 쓰기 위해 만든 인터페이스를 의미합니다. 이 인터페이스는 모두 리눅스 시스템에서 제공하는 GNU C 라이브러리 내부에 구현돼 있습니다.


2 단계: 유저 공간에서 시스템 콜 실행

리눅스 시스템 저수준 함수를 호출하면 리눅스 시스템에서 제공하는 GNU C 라이브러리 내 코드가 실행합니다. 라이브러리 내부 ARM 어셈블리 코드 실행으로 시스템 콜을 발생합니다. 이 과정을 제대로 이해하려면 ARM에서 시스템 콜을 어떻게 처리하는지 살펴볼 필요가 있습니다.


3 단계: 커널 공간에서 시스템 콜 실행

시스템 콜이 실행하면 커널 공간으로 이동해서 시스템 테이블에 접근한 후 각 리눅스 저수준 함수(API) 종류별로 대응하는 시스템 콜 핸들러 함수로 분기합니다. sys_open(), sys_write() 그리고 sys_read() 함수들은 가상 파일 시스템을 통해 파일 시스템에 접근합니다. sys_clone() 그리고 sys_exit() 함수들은 프로세스 생성과 종료와 연관된 커널 드라이버에 있는 계층에 접근합니다.

시스템 콜 핸들러 함수는 리눅스 저수준 함수 앞에 sys_ 접두사가 붙는 경우가 대부분입니다. write() 함수는 sys_write() 함수, read() 함수는 sys_read() 함수에 대응합니다. 하지만 모든 시스템 콜 핸들러 함수가 이 규칙을 따르지는 않습니다. 리눅스 저수준 함수 fork()는 sys_clone() 시스템 콜 핸들러가 실행합니다.


4단계: 커널 공간에서 시스템 콜 핸들러 실행

시스템 콜 핸들러에서는 유저 공간에서 전달한 매개 인자에 오류를 점검 후 시스템 콜 종류에 따라 가상 파일 시스템 계층이나 프로세스 관리 함수에 접근합니다.


시스템 콜 핸들러에서는 유저 공간에서 전달한 매개 인자에 오류를 점검 후 시스템 콜 종류에 따라 가상 파일 시스템 계층이나 프로세스 관리 함수에 접근합니다.


여기까지 유저 공간에서 커널 공간까지 시스템 콜 처리 과정입니다.


시스템 콜 인터페이스 동작을 더 정확하게 이해하려면 ARM 프로세스에서 시스템 콜을 어떻게 처리하는지 알아야 합니다. 이 내용은 다음에 다룹니다.

유저 프로세스 입장에서 시그널이란 


시그널이란 무엇일까요?

커널 입장에서 시그널은 프로세스에게 보내는 단순한 형태의 메시지라고 할 수 있습니다.

시그널 정보와 PID를 프로세스에게 전달하는 것입니다.


유저 프로세스 입장에서 프로세스는 무엇일까요? 유저 프로세스 관점으로 시그널은 실행 흐름을 제어하는 비동기적인 중단입니다. 이렇게 유저 프로세스와 커널 입장에서 시그널은 약간 다른 관점에서 볼 수 있습니다.


이번에 먼저 유저 프로세스 관점으로 시그널을 살펴보겠습니다.

여러분이 너무 보고 싶은 책이 있다고 가정하겠습니다. 

3시간 동안 읽을 수 있는 책 분량이라 주말에 책을 읽기 시작했습니다. 3시간 동안 아무런 방해를 받지 않고 책을 읽을 수 있으면 좋겠지만 이런 상황은 이상적인 환경입니다. 집에서 인터폰 벨리 울리던가 전화가 온다던가 여러 종류 중단이 발생 수 있기 때문입니다.


이런 중단은 예상하지 않았던 상황에서 발생하며 소프트웨어에서는 비동기적인 이벤트라고도 말합니다.


인터폰이 울리거나 회사나 친구한테 전화가 오면 우리는 보통 적절한 대응을 합니다. 보통 전화를 받거나 인터폰을 받고 대화를 합니다. 만약 여러분이 임베디드 리눅스 개발자인데 주말에 회사에서 전화가 왔다면 어떻게 할까요? 대부분 전화를 받을 것입니다. 시급한 문제가 생겼을 때 회사에서 개발자에게 전화를 하기 때문입니다. 하지만 가끔 중요하지 않은 다른 전화(광고, 부동산 투자)가 오면 전화를 안 받을 수도 있습니다.


프로세스도 마찬가지입니다. 유저 레벨 프로세스 기준으로 우리가 책을 읽는 것과 마찬가지로 정해진 시나리오에 따라 어떤 작업을 수행한다고 가정합시다. 책을 끝가지 방해 받지 않고 읽었으면 좋겠으나 유저 레벨 프로세스도 마찬가지로 예상치 못한(비동기적인) 중단으로 작업 흐름이 끊어 질 수 있습니다.


이렇게 인터폰이 울리거나 전화가 오는 것과 같이 유저 프로세스도 일을 하다가 비동기적인 중단을 겪을 수 있습니다. 이를 리눅스에서는 시그널이라고 하며 유저 프로세스는 시그널에 대해 이미 정해진 처리를 해줘야 합니다.


이런 유형의 다른 대표적인 중단은 인터럽트를 들 수 있습니다. 유저 프로세스 입장에서 시그널도 예상치 않았던 비동기적인 이벤트라고 볼 수 있습니다. 


대표적인 비동기적인 이벤트로 시그널이 발생하는 상황을 생각해봅시다.

1.리눅스 터미널에서 Ctl+C 키를 눌러서 프로세스를 종료

2.리눅스 터미널에서 다음 커맨드로 프로세스를 강제 종료

kill -9 [PID]

3.리눅스 커널에서 특정 조건에서 해당 프로세스를 종료


이렇게 언제 발생할지 모르는 비동기적인 중단(이벤트)에 대해 적절한 처리를 해줘야 합니다.


리눅스 커널에서도 자체적으로 시그널을 발생할 수 있습니다. 한 가지 예로 OOM(Out-of-memory) Killer를 들 수 있습니다. 잔여 메모리가 매우 부족할 때 OOM Killer 모듈은 프로세스를 강제 종료시켜서 메모리를 확보합니다. 종료할 프로세스에게 시그널을 전달합니다. 


안드로이드 시스템에서 OOM Killer가 실행하기 전 메모리 부족을 방지하기 위해 Lowmemory Killer란 모듈을 실행합니다. OOM Killer와 마찬가지로 프로세스를 종료시켜서 메모리를 확보합니다. 이 때도 종료할 프로세스에게 시그널을 전달합니다. 이 내용은 다음 소절에서 자세히 다룹니다.


시그널 번호와 동작 알아보기


책을 읽다가 발생하는 여러 비동기적인 이벤트(인터폰, 전화)가 있듯이, 유저 레벨 프로세스 동작 중에 발생할 수 있는 시그널도 여러 가지가 있습니다. 유닉스나 리눅스 커널 버전이 달라도 시그널 종류는 비슷합니다. 시그널은 POSIX 규약으로 정의된 표준이며 이제 맞게 리눅스 시스템 개발자가 구현하기 때문입니다. 다음은 라즈베리파이 리눅스 커널 4.14.70 버전에서 지원하는 시그널 번호를 확인한 결과입니다.

pi@raspberrypi:~ $ kill -l

 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP

 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1

11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM

16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP

21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ

26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR

31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3

38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8

43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13

48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12

53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7

58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2

63) SIGRTMAX-1 64) SIGRTMAX


시그널은 POSIX(Portable Operating System Interface) 규약에 정한 표준입니다. 어플리케이션 이식성을 높이기 위한 시도로 어플리케이션이 다양한 유닉스 계열 운영체제에서 구동할 수 있게 정한 것입니다. 리눅스 시스템 개발자들은 POSIX 규약에 따라 시스템 코드를 설계하고 코드를 구현합니다. 


1~34번까지는 유닉스 계열 운영체제(리눅스 포함)에서 같은 시그널 종류와 번호를 확인할 수 있습니다. 이를 정규 시그널이라고도 말합니다. 대신 35~63 시그널은 리얼 타임 시그널입니다.


정규 시그널과 리얼 타임 시그널의 차이점은 무엇일까요? 가장 큰 차이는 시그널 큐 처리 방식입니다. 정규 시그널은 같은 종류의 시그널을 연달아 보내면 프로세스는 한 가지 시그널만 받아 처리하지만 리얼 타임 시그널은 모듀 큐에 쌓아서 처리를 합니다.


각각 시그널은 int 형 정수로 선언되어 있는데 라즈베리파이에서 다음 해더 파일에서 시그널 종류별 int형 정수 번호를 확인할 수 있습니다.

root@raspberrypi:/usr/include # cat arm-linux-gnueabihf/asm/signal.h

#define SIGHUP          1

#define SIGINT           2

#define SIGQUIT         3

#define SIGILL            4

#define SIGTRAP         5

#define SIGABRT         6

#define SIGIOT           6

#define SIGBUS          7

#define SIGFPE           8

#define SIGKILL          9

#define SIGUSR1        10

#define SIGSEGV        11


유저 어플리케이션에서 시그널을 처리하는 함수를 작성하면 위와 같이 각 시그널 종류 별로 정의된 정수 값으로 시그널을 처리합니다.


유저 공간에서 정의된 시그널 번호는 리눅스 커널에서도 같은 번호로 관리합니다. 

[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/uapi/asm/signal.h]

#define SIGHUP 1

#define SIGINT 2

#define SIGQUIT 3

#define SIGILL 4

#define SIGTRAP 5

#define SIGABRT 6

#define SIGIOT 6

#define SIGBUS 7

#define SIGFPE 8

#define SIGKILL 9

#define SIGUSR1 10

#define SIGSEGV 11


32개 시그널 중 자주 활용하는 시그널을 정리하면 다음과 같습니다.

시그널 동작

SIGHUP 프로세스 제어 터미널이 종료될 때 세션 리더에게 전달, 터미널을 읽어버렸을때 발생

SIGINT 터미널 인터럽트 신호로(Ctl+C)키나 DELETE 키를 입력했을때 발생

SIGQUIT 사용자가 종료 문자(Ctl-\) 실행

SIGILL 유저가 유효하지 않은 명령어 실행 시도

SIGTRAP 트레이스 혹은 브레이크 포인트 실행

SIGABRT 프로세스가 비정상적인 종료 시중단 신호로 abort()에서 보냄

SIGIOT 비동기적인 I/O 이벤트 처리 시 보냄

SIGBUS 유효하지 않은 메모리 공간에 접근하거나 하드웨어 장애를 일으킬 때 커널이 생성

SIGFPE 부동 소수점을 연산 도중 오버플로우나 언더플로우가 발생하면 익셉션으로 발생하는 시그널

SIGKILL kill() 함수를 호출하면 프로세스를 종료시킴

SIGUSR1

SIGUSR2 유저 공간에서 처리하기 위해 정의하며 커널은 이 시그널을 쓰지 않음

SIGSEGV 유효하지 않은 메모리 접근을 시도할 때 커널이 해당 프로세스에 전달함

읽기나 쓰기 권한이 없는 메모리 공간에 접근하거나 실행할 수 없는 코드를 실행할 때 발생함

SIGUSR2 유저 공간에서 처리하기 위해 정의하며 커널은 이 시그널을 쓰지 않음

SIGPIPE 닫힌 파이프에 열고 쓸 때 실행

SIGALRM alarm() 함수가 자신을 실행한 프로세스에게 전달

SIGCHLD 프로세스가 종료할 때 커널은 해당 프로세스의 부모 프로세스에게 전달


유저 레벨 프로세스가 리눅스 저수준 표준 함수를 호출해서 시그널을 발생할 수 있습니다. 또한 어떤 프로세스가 종료할 때 부모 프로세스에게 자식 프로세스가 종료한다는 정보를 SIGCHLD 시그널로 알립니다.


만약 특정한 프로세스에게 시그널을 전달하고 싶을 때 어떻게 하면 될까요? 리눅스 터미널에서 kill 명령어를 쓰면 됩니다.


라즈베리파이에서 X-Terminal을 2개 열고 다음 명령어를 입력합시다.

1 root@raspberrypi:/usr/include # ps -ely | grep bash

2 S 1000 500 432 0 80 0 4096 1645 poll_s tty1 00:00:00 bash

3 S 1000 1150 1146 0 80 0 4192 1628 wait pts / 0 00:00:00 bash

4 S 0 1355 1350 0 80 0 3376 1433 wait pts / 0 00:00:00 bash

5 S 1000 1386 1146 0 80 0 3964 1628 poll_s pts / 1 00:00:00 bash

6 root@raspberrypi:# kill -SIGKILL 1386


위에서 5번 출력 결과를 보면 가장 마지막에 실행된 bash 프로세스의 PID가 1386이니 위와 같이 kill 명령어에 -SIGKILL 옵션을 줘서 실행하니 프로세스는 강제 종료합니다.



라즈베리파이 리눅스 시스템에서 kill 명령어 메뉴얼을 확인하면 다음과 같습니다.

root@raspberrypi:/home/pi# info kill

Up: Process control


24.1 ‘kill’: Send a signal to processes

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

The ‘kill’ command sends a signal to processes, causing them to

terminate or otherwise act upon receiving the signal in some way.


kill은 프로세스에게 시그널을 전달하는 명령어인데 대부분 프로세스를 종료하거나

시그널 종류에 따라 정해진 동작을 처리한다는 사실을 알 수 있습니다.


커널에서 시그널은 어떻게 처리할까? 


리눅스 커널 입장에서 시그널은 프로세스 간 통신을 위한 간단한 인터페이스입니다. 커널은 상황에 따라 시그널을 생성하고 전달해주는 역할을 수행합니다.


리눅스 커널에서 시그널에 대한 처리는 2단계로 나눌 수 있습니다.


1> 시그널 생성

유저 공간에서 시그널을 설정하면 커널은 해당 프로세스의 태스크 디스크립터에 시그널 정보를 써줍니다. 시그널을 받을 프로세스 스택 최상단 주소에 있는 struct thread_info flags 멤버에 _TIF_SIGPENDING 매크로를 써 줍니다. 시그널을 받을 프로세스에게 누군가 시그널을 생성했고 해당 시그널이 전달될 것이라고 알려주는 것입니다. 이후 시그널을 받을 프로세스를 깨웁니다.


2> 시그널 처리

시그널을 받을 프로세스가 시스템 콜이나 인터럽트 처리를 마무리한 이 후 시그널을 처리합니다.


커널은 시그널 종류에 따라 유저 프로세스가 정해진 동작을 수행하도록 다음 동작을 합니다.

 - 시그널 핸들러를 설정 안했을 경우

   : SIGINT, SIGKILL 시그널인 경우 프로세스를 종료시킵니다.

 - 시그널 핸들러를 설정했을 경우

   :  시그널 핸들러 주소를 ARM 프로그램 카운터 정보에 써줘서 시그널 핸들러를 실행시킵니다.


커널은 시그널 핸들러를 실행시켜 줄 뿐 시그널 종류에 따라 세부적인 처리를 할 수가 없습니다. 프로세스에게 전달하는 정보는 시그널 번호가 전부이며 표준 시그널에는 인자나 메시지 또는 그외 정보를 전달할 수 없습니다.


커널은 시그널을 대부분 프로세스를 종료할 때 프로세스나 스레드 그룹에 전달하는 메시지 형태로 사용합니다.


시그널을 발생했으나 아직 전달되지 않은 시그널을 펜딩 중인 시그널(pending signal)이라고 합니다. 특정 타입의 펜딩 시그널은 프로세스당 항상 하나만 존재합니다. 같은 시그널을 동일한 프로세스에게 전달하면 시그널 큐에서 대기하는 것이 아니라 그냥 폐기됩니다. 

기본 유저 레벨 프로세스 실행 실습 및 ftrace 로그 분석

라즈베리파이에서 X-terminal 프로그램을 실행해서 셸을 엽시다. 

root@raspberrypi:/boot# ps -ely | grep bash
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000   541   443  0  80   0  4024  1645 poll_s tty1     00:00:00 bash
S  1000   880   878  0  80   0  4008  1628 wait   pts/0    00:00:00 bash
S     0   977   972  0  80   0  3284  1416 wait   pts/0    00:00:00 bash
S  1000   993   989  0  80   0  3960  1628 poll_s pts/1    00:00:00 bash

grep bash 명령어로 현재 실행 중인 프로세스 중에 bash 프로세스를 출력합니다. 출력 결과 4개 bash 프로세스 목록을 볼 수 있습니다.

이 상태에서 X-terminal 셸을 하나 더 실행하고 다음 명령어를 입력해서 bash 프로세스 목록을 확인합시다.
root@raspberrypi:/boot# ps -ely | grep bash
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000   541   443  0  80   0  4024  1645 poll_s tty1     00:00:00 bash
S  1000   880   878  0  80   0  4008  1628 wait   pts/0    00:00:00 bash
S     0   977   972  0  80   0  3284  1416 wait   pts/0    00:00:00 bash
S  1000   993   989  0  80   0  3960  1628 poll_s pts/1    00:00:00 bash
S  1000  1027   878  3  80   0  4036  1628 poll_s pts/2    00:00:00 bash

이전에 출력한 결과와 비교해봅시다. 맨 마지막 줄 로그를 보면 pid가 1027인 bash 프로세스가 보입니다. 셸을 하나 더 열고 “ps –ely” 명령어를 입력하니 bash(pid:1027)과 같이 새로 생성된 프로세스를 볼 수 있습니다. 이렇게 새로운 프로그램을 실행하면 이에 해당하는 프로세스가 생성됩니다.

라즈베리파이 X-Terminal 셸 화면을 마우스로 더블 클릭하는 순간 라즈베리파이 배경 화면을 처리하는 프로세스가 이벤트를 받아서 bash라는 프로세스를 생성합니다. 이 때 리눅스 저수준 함수인 fork()를 호출합니다. 이렇게 유저 레벨 프로세스는 셸이나 다른 프로세스를 통해서 실행을 시작합니다. 유저 레벨 프로세스는 혼자서 실행할 수 없습니다.

이번에는 라즈베리파이에서 소스 에디터로 많이 쓰는 Geany란 프로그램을 열겠습니다.
root@raspberrypi:/boot# ps -ely | grep geany
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000   989   671  1  80   0 28276 25827 poll_s ?        00:00:06 geany

Geany 프로그램을 하나 더 열고 다음 명령어를 입력합시다.
root@raspberrypi:/boot# ps -ely | grep geany
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000   989   671  1  80   0 28276 25827 poll_s ?        00:00:06 geany
S  1000  1297   671 38  80   0 25204 13533 poll_s ?        00:00:01 geany

PID가 1297인 geany 프로세스가 생성됐습니다.

프로세스를 어렵게 생각할 필요가 없습니다. 셸이나 geany이란 프로그램을 실행하면 메모리에서 실행하는 것이 프로세스입니다. 유저 레벨에서 실행하는 프로세스는 이렇게 유저 동작으로 생성됩니다.

이번에 리눅스 시스템 프로그래밍으로 프로세스를 생성해 봅시다. 소스 코드는 다음과 같으니 같이 입력해 봅시다.
1 #include <stdio.h>
2 #include <unistd.h>
3
4 #define PROC_TIMES 500
5 #define SLEEP_DURATION 3  // second unit
6
7 int main() 
8 {
9 int proc_times = 0;
10
11 for(proc_times = 0; proc_times < PROC_TIMES; proc_times++) {
12 printf("raspbian tracing \n");
13 sleep(SLEEP_DURATION);
14 }
15
16 return 0;
17 }

위와 같은 프로그램을 리눅스 시스템 프로그램이라고 합니다. 리눅스 시스템을 관리하는 sleep()이나 fork() 함수를 직접 호출하기 때문에 응용 프로그램 입장에서 저수준 프로그래밍이라고도 합니다. 위 함수를 리눅스 시스템 저수준 함수(API)라고 부르겠습니다.

위 코드는 다음 코드 이외에 다른 동작을 하지 않습니다.
11 for(proc_times = 0; proc_times < PROC_TIMES; proc_times++) {
12 printf("raspbian tracing \n");
13 sleep(SLEEP_DURATION);
14 }

소스 코드를 잠깐 봅시다.

12번째 줄 코드와 같이 “raspbian tracing”이란 메시지를 셸로 출력하고 13번째 줄 코드와 같이 3초 동안 휴면에 들어갈 뿐입니다.

위 코드를 입력한 다음 raspbian_test.c 란 이름으로 저장합시다. 컴파일을 쉽게 하기 위해 다음과 같이 코드를 작성하고 파일 이름을 Makefile으로 저장합시다.
raspbian_proc: raspbian_test.c
gcc -o raspbian_proc raspbian_test.c

“make” 명령어로 위와 같은 메이크 파일을 실행하면 raspbian_proc이란 실행 파일이 생성됩니다.

메이크 파일은 여러 모듈을 일일이 컴파일 명령어를 입력하기 어려우니 컴파일 설정 속도를 빠르게 하기 위해 고안된 겁니다. 실전 프로젝트에서 메이크 파일은 자주 쓰니 잘 알아둡시다.

make란 명령어를 입력해서 raspbian_test.c 파일을 컴파일하면 raspbian_proc이란 프로그램을 생성할 수 있습니다.

raspbian_proc 이란 프로세스가 어떻게 생성되고 실행되는지 파악하려면 다음과 같이 ftrace 로그를 설정할 필요가 있습니다. 코드를 봅시다.
1  #!/bin/sh
3  echo 0 > /sys/kernel/debug/tracing/tracing_on
4  sleep 1
5  echo "tracing_off"
6
7 echo 0 > /sys/kernel/debug/tracing/events/enable
8 sleep 1
9 echo "events disabled"
10
11 echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
12 sleep 1
13 echo "set_ftrace_filter init"
14 
15 echo function > /sys/kernel/debug/tracing/current_tracer
16 sleep 1
17 echo "function tracer enabled"
18
19 echo SyS_clone do_exit > /sys/kernel/debug/tracing/set_ftrace_filter
20 echo _do_fork copy_process* >> /sys/kernel/debug/tracing/set_ftrace_filter
21
22 sleep 1
23 echo "set_ftrace_filter enabled"
24
25 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
26 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable
27 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_fork/enable
28 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_exit/enable
29
30 echo 1 > /sys/kernel/debug/tracing/events/signal/enable
31
32 sleep 1
33 echo "event enabled"
34
35 echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
36 echo 1 > /sys/kernel/debug/tracing/options/sym-offset
37 echo "function stack trace enabled"
38
39 echo 1 > /sys/kernel/debug/tracing/tracing_on
40 echo "tracing_on"
조금 더 알아보기
ftrace 에서 시스템 콜 핸들러 함수 심볼(이름)을 Alias 심볼로 씁니다.

예를 들어 sys_write() 함수에 대한 alias 심볼은 SyS_write와 같습니다.
다음 전처리 코드는 fs/read_write.c 파일에 위치한 write 시스템 콜 핸들러 함수 선언부입니다.
[out/fs/.tmp_read_write.i]
1 long sys_write(unsigned int fd, const char * buf, size_t count) __attribute__((alias("SyS_write")));
2
3 [https://elixir.bootlin.com/linux/v4.14.70/source/fs/read_write.c]
4 SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
5 size_t, count)
6 {
7 struct fd f = fdget_pos(fd);
8 ssize_t ret = -EBADF;

1번째 줄 코드를 보면 함수 인자 오른쪽에 다음과 같은 코드를 볼 수 있습니다.
__attribute__((alias("SyS_write")));

GCC 컴파일러가 함수 컴파일시 alias 심볼을 적용한 것인데 sys_write() 함수에 대한 alias 심볼이 SyS_write입니다.

ftrace 로그에서 SyS_xxx 로 어떤 함수를 표현하면 실제 함수 이름은 sys_xxx() 이라고 생각해도 좋습니다.

따라서 ftrace 로그 설정 시 set_ftrace_filter로 SyS_clone 함수로 지정한 겁니다.
19 echo SyS_clone do_exit > /sys/kernel/debug/tracing/set_ftrace_filter

이렇게 지정하면 ftrace는 실제 리눅스 커널 코드에서 sys_clone 함수를 추적(트레이싱)합니다.

위와 같이 코드를 작성한 후 clone_process_debug.sh 와 같은 이름을 저장한 후 다음과 같이 이 셸 스크립트를 실행합시다.
./clone_process_debug.sh

위 셸 스크립트를 실행하면 5~6초 내 ftrace 로그 설정이 끝납니다. 이후 raspbian_test.c 파일을 컴파일하면 생성되는 raspbian_proc이란 프로그램을 다음 명령어로 실행합시다. 
root@raspberrypi:/home/pi# ./raspbian_proc 
raspbian tracing 
raspbian tracing 
raspbian tracing 
raspbian tracing 
raspbian tracing 
raspbian tracing

raspbian_proc 이란 프로그램을 실행하니 3초 간격으로 “raspbian tracing”이란 메시지를 출력합니다. 소스 코드에서 구현한 대로 실행합니다.

raspbian_proc 프로그램을 실행했으니 이에 해당하는 프로세스가 생성됐을 것이라 예상할 수 있습니다. 이번에는 “ps -ely” 명령어를 입력해서 프로세스 목록을 확인합시다.
root@raspberrypi:/home/pi# ps -ely
1 S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
2 S     0     1     0  0  80   0  5956  6991 SyS_ep ?        00:00:02 systemd
3 S     0     2     0  0  80   0     0     0 kthrea ?        00:00:00 kthreadd
...
4
5 S     0   895   890  0  80   0  3420  1448 wait   pts/0    00:00:00 bash
6 S  1000   991   685  0  80   0  7500  7842 poll_s ?        00:00:00 ibus-engine-han
...
7  S     0  1078  1073  0  80   0  3244  1416 wait   pts/2    00:00:00 bash
8  I     0  1079     2  0  80   0     0     0 worker ?        00:00:00 kworker/3:2
9  I     0  2302     2  0  80   0     0     0 worker ?        00:00:00 kworker/0:1
10 S     0 17082   895  0  80   0   344   453 hrtime pts/0    00:00:00 raspbian_proc
11 I     0 17084     2  0  80   0     0     0 worker ?        00:00:00 kworker/u8:1
12 I     0 17085     2  0  80   0     0     0 worker ?        00:00:00 kworker/1:0
13 R     0 17086  1078  0  80   0  1156  1918 -      pts/2    00:00:00 ps

프로세스 목록 10번째 항목을 보면 pid가 17082인 raspbian_proc 프로세스가 보입니다. 리눅스 시스템에서 raspbian_proc 프로세스가 READY 상태이란 의미입니다. 
1 S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
5 S     0   895   890  0  80   0  3420  1448 wait   pts/0    00:00:00 bash
...
10 S     0 17082   895  0  80   0   344   453 hrtime pts/0    00:00:00 raspbian_proc

1번째 줄 로그에서 PPID가 보입니다. 이 정보는 부모 프로세스의 pid를 의미합니다. raspbian_proc 프로세스의 부모 프로세스는 pid가 895입니다. pid가 895인 프로세스를 확인하니 프로세스 목록 5번째 항목과 같이 bash 프로세스입니다. raspbian_proc 프로세스의 부모 프로세스는 bash임을 알 수 있습니다.

raspbian_proc 프로세스의 부모 프로세스는 왜 bash(pid:895) 일까요? raspbian_proc 프로세스를 실행할 때 X-Terminal bash 셸에서 다음 명령어로 실행했기 때문입니다.
root@raspberrypi:/home/pi# ./raspbian_proc 

이렇게 유저 레벨 프로세스는 셸이나 다른 프로세스를 통해서 실행을 시작합니다. 만약 라즈베리파이 바탕 화면에 있는 아이콘을 클릭해서 프로그램을 시작해서 유저 레벨 프로세스를 실행했다고 가정합시다. 이 경우 바탕화면을 제어하는 프로세스가 부모 프로세스가 됩니다.

raspbian_proc 프로세스를 이렇게 15초 동안 실행시킨 다음에 다른 x-terminal 셸을 실행을 실행해서 다음과 같이 raspbian_proc 프로세스를 강제 종료해봅시다.
root@raspberrypi:/home/pi# kill -9  17082

kill 명령어로 pid를 지정하면 강제로 지정한 프로세스를 종료합니다. -9는 강제로 프로세스를 종료시키는 옵션입니다.

다음 명령어를 입력해서 kill이란 명령어가 어떤 의미인지 확인합시다.
root@raspberrypi:/home/pi# info kill

24.1 ‘kill’: Send a signal to processes
=======================================

The ‘kill’ command sends a signal to processes, causing them to
terminate or otherwise act upon receiving the signal in some way.
Alternatively, it lists information about signals.  Synopses:

     kill [-s SIGNAL | --signal SIGNAL | -SIGNAL] PID...
     kill [-l | --list | -t | --table] [SIGNAL]...

   Due to shell aliases and built-in ‘kill’ functions, using an
unadorned ‘kill’ interactively or in a script may get you different
functionality than that described here.  Invoke it via ‘env’ (i.e., ‘env
kill ...’) to avoid interference from the shell.

매뉴얼 내용과 같이 kill 명령어는 프로세스를 종료하는 역할을 수행합니다.

이번에는 다음과 같은 셸 스크립트를 실행해서 ftrace 로그를 추출합시다.
#!/bin/sh

echo 0 > /sys/kernel/debug/tracing/tracing_on
echo "ftrace off"

sleep 3

cp /sys/kernel/debug/tracing/trace . 
mv trace ftrace_log.c

위 코드를 get_ftrace.sh 이름으로 저장해놓고 ftrace 로그를 받을 때 다음 명령어를 실행합니다.
root@raspberrypi:/home/pi#./get_ftrace.sh

그러면 같은 폴더에 ftrace.c이란 파일이 생성됐음을 확인할 수 있습니다.

이제까지 프로세스 생성과 종료 과정을 저장한 ftrace 로그를 추출하기 위해 진행한 과정을 정리하면 다음과 같습니다.
1. 다음 명령어로 프로세스 실행
root@raspberrypi:/home/pi# ./raspbian_proc 

2. ftrace 로그 설정 및 시작
3. ps 명령어로 프로세스 동작 확인

4. raspbian_proc 프로세스 종료
root@raspberrypi:/home/pi# kill -9  17082

5. ftrace 로그 추출

커널 공간에서 raspbian_proc 이란 프로세스가 어떤 코드 흐름으로 생성하고 종료했는지 ftrace 로그로 알아봅시다.

분석할 전체 ftrace 로그는 다음과 같습니다.
1 bash-895 [003] .... 909.253260: SyS_clone+0x14/0x38 <-ret_fast_syscall+0x0/0x28
2 bash-895 [003] .... 909.253295: <stack trace>
3 bash-895 [003] .... 909.253298: _do_fork+0x14/0x3ec <-SyS_clone+0x30/0x38
4 bash-895 [003] .... 909.253310: <stack trace>
5 lxpanel-730 [002] d.h.   909.253310: sched_wakeup: comm=ibus-x11 pid=717 prio=120 target_cpu=002
6 bash-895 [003] .... 909.253312: copy_process.part.5+0x14/0x17d8 <-_do_fork+0xb0/0x3ec
7 bash-895 [003] .... 909.253324: <stack trace>
8 => ret_fast_syscall+0x0/0x28
...
9 bash-895 [003] .... 909.253776: sched_process_fork: comm=bash pid=895 child_comm=bash child_pid=17082
10 <idle>-0 [002] d... 909.253809: sched_switch: prev_comm=swapper/2 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=bash next_pid=17082 next_prio=120
11 bash-895 [003] d... 909.254159: sched_switch: prev_comm=bash prev_pid=895 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=1047 next_prio=120
12 <idle>-0 [000] dnh. 909.254206: sched_wakeup: comm=lxterminal pid=876 prio=120 target_cpu=000
13 <idle>-0 [000] d... 909.254215: sched_switch: prev_comm=swapper/0 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=lxterminal next_pid=876 next_prio=120
14 kworker/u8:0-1047 [003] d... 909.254221: sched_switch: prev_comm=kworker/u8:0 prev_pid=1047 prev_prio=120 prev_state=t ==> next_comm=swapper/3 next_pid=0 next_prio=120
15 raspbian_proc-17082 [002] d.s. 909.254409: sched_wakeup: comm=rcu_sched pid=8 prio=120 target_cpu=002
16 raspbian_proc-17082 [002] d... 909.257817: sched_switch: prev_comm=raspbian_proc prev_pid=17082 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
17 <idle>-0 [002] dnh. 912.257874: sched_wakeup: comm=raspbian_proc pid=17082 prio=120 target_cpu=002
18 raspbian_proc-17082 [002] d... 912.257957: sched_switch: prev_comm=raspbian_proc prev_pid=17082 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
19 <idle>-0  [002] dnh. 915.258028: sched_wakeup: comm=raspbian_proc pid=17082 prio=120 target_cpu=002          
20 <idle>-0 [002] d... 915.258044: sched_switch: prev_comm=swapper/2 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=raspbian_proc next_pid=17082 next_prio=120          
21 <idle>-0     [003] dnh.   915.258098: sched_wakeup: comm=kworker/u8:1 pid=17084 prio=120 target_cpu=003   
22 raspbian_proc-17082 [002] d...   915.258110: sched_switch: prev_comm=raspbian_proc prev_pid=17082 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
23 raspbian_proc-17082 [002] d...   933.741224: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0
24 kworker/u8:2-137 [001] d...   933.741230: sched_switch: prev_comm=kworker/u8:2 prev_pid=137 prev_prio=120 prev_state=t ==> next_comm=swapper/1 next_pid=0 next_prio=120
25 raspbian_proc-17082 [002] .... 933.741230: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
26 raspbian_proc-17082 [002] .... 933.741270: <stack trace>
27 => do_signal+0x300/0x3d4
28 => do_work_pending+0xb4/0xcc
29 => slow_work_pending+0xc/0x20
30 lxpanel-730 [000] d... 933.741295: sched_switch: prev_comm=lxpanel prev_pid=730 prev_prio=120 prev_state=S ==> next_comm=Xorg next_pid=454 next_prio=120
31 Xorg-454 [000] dn.. 933.741333: sched_wakeup: comm=openbox pid=723 prio=120 target_cpu=000
32 Xorg-454 [000] d... 933.741346: sched_switch: prev_comm=Xorg prev_pid=454 prev_prio=120 prev_state=R ==> next_comm=openbox next_pid=723 next_prio=120
33 raspbian_proc-17082 [002] .... 933.741609: sched_process_exit: comm=raspbian_proc pid=17082 prio=120
34 ibus-x11-717 [001] d... 933.741639: sched_switch: prev_comm=ibus-x11 prev_pid=717 prev_prio=120 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_prio=120
35 openbox-723 [000] d... 933.741673: sched_switch: prev_comm=openbox prev_pid=723 prev_prio=120 prev_state=S ==> next_comm=Xorg next_pid=454 next_prio=120
36 raspbian_proc-17082 [002] d... 933.741693: signal_generate: sig=17 errno=0 code=2 comm=bash pid=895 grp=1 res=0

먼저 유저 공간에서 프로세스가 생성하면 다음 그림과 같은 흐름으로 _do_fork() 함수가 실행합니다.
                    
리눅스에서 실행 공간은 메모리 접근과 실행 권한 기준으로 유저 공간과 커널 공간으로 구분할 수 있습니다. 유저 공간은 유저 모드, 커널 공간은 커널 모드에서 각각 에서 실행하는 메모리 공간입니다. 유저 공간과 유저 모드는 거의 비슷한 의미로 씁니다.

리눅스 시스템 저수준 함수로 fork() 함수를 호출하면 유저 공간에서 코드 공간으로 실행 흐름 공간을 이동하는 시스템 콜이 발생하고 커널 모드로 실행 흐름이 변경됩니다. 이후 커널 모드에서 시스템 콜 번호에 해당하는 핸들러 함수가 호출됩니다. 이 함수가 sys_clone()입니다.

위 그림이 실제 ftrace 로그로 어떻게 출력하는지 확인합시다.

먼저 1~8번째 줄 로그를 분석하겠습니다.
1 bash-895 [003] .... 909.253260: SyS_clone+0x14/0x38 <-ret_fast_syscall+0x0/0x28
2 bash-895 [003] .... 909.253295: <stack trace>
3 bash-895 [003] .... 909.253298: _do_fork+0x14/0x3ec <-SyS_clone+0x30/0x38
4 bash-895 [003] .... 909.253310: <stack trace>
5 lxpanel-730 [002] d.h.   909.253310: sched_wakeup: comm=ibus-x11 pid=717 prio=120 target_cpu=002
6 bash-895 [003] .... 909.253312: copy_process.part.5+0x14/0x17d8 <-_do_fork+0xb0/0x3ec
7 bash-895 [003] .... 909.253324: <stack trace>
8 => ret_fast_syscall+0x0/0x28
조금 더 알아보기: ftrace 로그 한줄 한줄을 실행하는 주체는 프로세스입니다. 모든 ftrace 가장 왼쪽에서 프로세스 이름과 pid를 볼 수 있습니다.

위 ftrace 로그에서는 가장 왼쪽에 “bash-895” 메시지가 보이는데 이 정보를 어떻게 해석해야 할까요? 이 정보는 pid가 895인 bash 프로세스를 의미합니다. 5번째 줄 로그 가장 왼쪽에서도 “lxpanel-730”이란 메시지를 볼 수 있습니다. 역시 pid가 730인 lxpanel이란 프로세스를 의미합니다.

로그들이 어지럽게 섞여 있는데 위 로그는 “bash-895”이란 프로세스가 다음 함수 흐름(콜스택)으로 실행 중인 상태입니다.
1 => copy_process.part.5+0x14/0x17d8
2 => _do_fork+0x14/0x3ec
3 => SyS_clone+0x14/0x38
4 => ret_fast_syscall+0x0/0x28

함수 호출 방향은 4에서 1번 로그 방향입니다. 시스템 콜 함수인 SyS_clone() 함수에서 _do_fork() 함수를 호출합니다.

다음 9번째 줄 로그를 봅시다.
9 bash-895 [003] .... 909.253776: sched_process_fork: comm=bash pid=895 child_comm=bash child_pid=17082

sched_process_fork이란 ftrace 이벤트 메시지와 함께 디버깅 정보를 출력합니다. pid가 895인 bash 프로세스가 pid가 17082인 자식 프로세스를 생성한다는 의미입니다. 부모 프로세스가 자식 프로세스를 생성할 때는 프로세스 이름은 그대로 가져갑니다. 이후 프로세스 이름을 자식 프로세스 이름으로 변경합니다.

task_rename 이란 ftrace 이벤트를 키면 다음과 같은 정보를 볼 수 있습니다.
raspbian_proc-17083  [003] .... 909.253836 task_rename: pid=17082 oldcomm=bash newcomm=raspbian_proc oom_score_adj=0

15~23번째 줄 로그는 raspbian_proc(pid:17083) 프로세스가 실행하는 정보입니다. 
15 raspbian_proc-17082 [002] d.s. 909.254409: sched_wakeup: comm=rcu_sched pid=8 prio=120 target_cpu=002
16 raspbian_proc-17082 [002] d... 909.257817: sched_switch: prev_comm=raspbian_proc prev_pid=17082 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
17 <idle>-0 [002] dnh. 912.257874: sched_wakeup: comm=raspbian_proc pid=17082 prio=120 target_cpu=002
18 raspbian_proc-17082 [002] d... 912.257957: sched_switch: prev_comm=raspbian_proc prev_pid=17082 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
19 <idle>-0 [002] dnh. 918.258177: sched_wakeup: comm=raspbian_proc pid=17082 prio=120 target_cpu=002
20 <idle>-0 [002] d... 918.258193: sched_switch: prev_comm=swapper/2 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=raspbian_proc next_pid=17082 next_prio=120
21 raspbian_proc-17082 [002] d... 918.258250: sched_wakeup: comm=kworker/u8:1 pid=17084 prio=120 target_cpu=002
22 raspbian_proc-17082 [002] d... 918.258277: sched_switch: prev_comm=raspbian_proc prev_pid=17082 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:1 next_pid=17084 next_prio=120

15번째 줄 로그를 보면 909.254409초에 raspbian_proc(pid: 17082) 프로세스가 "rcu_sched" 프로세스를 깨웁니다. 이후 909.257817초에 16번째 줄 로그와 같이 "swapper/2" 프로세스로 스케줄링됩니다.

17번째 로그를 보면, 약 3초 후인 912.257874 초에 idle-0이란 프로세스가 raspbian_proc(pid: 17082) 프로세스를 깨우고, 18번째 로그와 같이 912.257957 초에  "swapper/2" 프로세스로 스케줄링됩니다.

sched_wakeup 이란 ftrace 이벤트는 프로세스를 깨우는 동작을 표현합니다. 정확히 설명을 드리면 스케줄러에게 해당 프로세스 실행 요청을 하는 것입니다.

같은 패턴으로 19~22번째 줄 로그를 보면 915.258028 초에 raspbian_proc(pid: 17082) 프로세스가 깨어나 실행합니다.

raspbian_proc_test.c 파일에서 3초 간격으로 sleep() 함수를 호출하고 raspbian tracing 이란 메시지를 출력하는 코드가 동작하는 것입니다. 구현된 코드와 같이 다음 시간 간격으로 3초 주기로 raspbian_proc 프로세스가 실행합니다.
909.254409 -> 912.257874  -> 915.258028

프로세스가 생성하는 1단계부터 raspbian_proc 프로세스가 3초 간격으로 실행하는 로그를 분석했습니다.

정리하면 유저 모드에서 fork() 이란 리눅스 시스템 저수준 함수를 호출하면 커널 공간에서 fork() 에 대응하는 시스템 콜 핸들러인 sys_clone() 함수를 호출하는 것입니다. 이 후 sys_clone() 함수에서 _do_fork() 함수 호출로 프로세스를 생성(복제)합니다. 

프로세스는 생성 후 바로 실행을 시작하는데 3초 주기로 실행합니다.

이번에는 raspbian_proc 프로세스가 종료하는 2단계 로그를 분석할 차례입니다. 이 과정은 다음 그림에서 확인할 수 있습니다.


우리는 다음 명령어로 raspbian_proc(pid:17082) 프로세스를 강제 종료시켰습니다.
root@raspberrypi:/home/pi# kill -9  17082

유저 레벨 프로세스가 유저 모드에서 명시적으로 exit() 함수를 호출해 종료하지 않고 kill 당한 것입니다.

다음과 같은 형식으로 kill 명령어를 입력하면 유저 레벨 프로세스를 강제 종료시킬 수 있습니다. 여기서 PID는 프로세스 아이디로 정수형 숫자를 의미합니다.
kill -9 [PID]
조금 더 알아보기

위 명령어를 입력하면 리눅스 커널에서는 다음과 같은 동작을 수행합니다.
1. 유저 모드에서 kill() 함수를 호출해서 커널 모드에서 sys_kill() 함수를 실행합니다.
2. “kill -9 [PID]” 명령어에서 PID에 해당하는 프로세스를 스케줄러에게 깨워 달라고 요청합니다.
3. 스케줄러가 PID에 해당하는 프로세스를 컨택스트 스위칭하면 해당 프로세스는 유저 공간으로 복귀 직전 slow_work_pending 이란 레이블을 실행합니다.
4. 위 그림 화살표 방향에서 보이는 함수 흐름으로 do_exit() 함수를 실행해 프로세스를 종료합니다.

kill 명령어는 프로세스를 종료시키는 시그널을 전달하는 명령어입니다.

kill이란 명령어는 유저 레벨에서 시그널로 전달하고 있고 커널에서 kill 명령어 실행에 할 수 있습니다. 조금 어렵게 설명을 드리면 커널에서 kill 명령어에 대응하는 함수 조합으로 kill 명령어 실행과 같은 동작을 할 수 있다는 것입니다.

유저 프로세스가 종료하려면 exit() 이란 함수를 실행하면 됩니다. 이 함수를 호출하면 시스템 콜이 실행되어 커널 공간에서 do_exit() 이란 함수가 실행합니다. 이번 소절에서는 kill 이란 명령어를 입력해서 프로세스를 강제 종료했습니다. exit() 저수준 함수를 호출해서 프로세스를 종료할 때와 실행 흐름이 다릅니다. 

다음은 raspbian_proc-17082 프로세스가 종료할 때 로그입니다.
25 raspbian_proc-17082 [002] .... 933.741230: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
26 raspbian_proc-17082 [002] .... 933.741270: <stack trace>
27 => do_signal+0x300/0x3d4
28 => do_work_pending+0xb4/0xcc
29 => slow_work_pending+0xc/0x20
30 lxpanel-730 [000] d... 933.741295: sched_switch: prev_comm=lxpanel prev_pid=730 prev_prio=120 prev_state=S ==> next_comm=Xorg next_pid=454 next_prio=120
31 Xorg-454 [000] dn.. 933.741333: sched_wakeup: comm=openbox pid=723 prio=120 target_cpu=000
32 Xorg-454 [000] d... 933.741346: sched_switch: prev_comm=Xorg prev_pid=454 prev_prio=120 prev_state=R ==> next_comm=openbox next_pid=723 next_prio=120
33 raspbian_proc-17082 [002] .... 933.741609: sched_process_exit: comm=raspbian_proc pid=17082 prio=120
34 ibus-x11-717 [001] d... 933.741639: sched_switch: prev_comm=ibus-x11 prev_pid=717 prev_prio=120 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_prio=120
35 openbox-723 [000] d... 933.741673: sched_switch: prev_comm=openbox prev_pid=723 prev_prio=120 prev_state=S ==> next_comm=Xorg next_pid=454 next_prio=120
36 raspbian_proc-17082 [002] d... 933.741693: signal_generate: sig=17 errno=0 code=2 comm=bash pid=895 grp=1 res=0

위 로그는 다음 명령어로 강제로 raspbian_proc(pid: 17082) 프로세스를 종료했을 때 출력합니다.
root@raspberrypi:/home/pi# kill -9  17082

25~29번째 줄 로그를 보면 raspbian_proc(pid: 17082)가 어떤 콜스택으로 종료하는지 알 수 있습니다.
25 raspbian_proc-17082 [002] .... 933.741230: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
26 raspbian_proc-17082 [002] .... 933.741270: <stack trace>
27 => do_signal+0x300/0x3d4
28 => do_work_pending+0xb4/0xcc
29 => slow_work_pending+0xc/0x20

raspbian_proc(pid:17082) 프로세스가 do_signal() 함수에서 do_exit() 함수를 호출해서 raspbian_proc(pid: 17082) 프로세스를 종료하는 동작입니다.

이번에 36번째 줄 로그를 보겠습니다.
36 raspbian_proc-17082 [002] d... 933.741693: signal_generate: sig=17 errno=0 code=2 comm=bash pid=895 grp=1 res=0

raspbian_proc(pid: 17082)가 자신이 종료한다는 정보를 시그널로 전달합니다. 
프로세스는 종료 과정에서 자신의 부모 프로세스에게 자신이 종료하고 있다는 시그널을 전달합니다.

프로세스가 생성할 때 SyS_clone() 이란 커널 함수가 호출되고 종료될 때 do_exit() 커널 함수가 호출된다는 점을 확인했습니다.

다음에 다른 리눅스 시스템 프로그램을 작성해서 유저 공간에서 생성된 프로세스가 어떤 과정으로 생성되고 소멸되는지 확인합시다.



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

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

유저 레벨에서 생성된 프로세스는 유저 공간에서 프로세스를 생성하는 라이브러리(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)에서 스레드 그룹 여부를 점검할 뿐입니다.


+ Recent posts