2017년 2월 28일 화요일

Efficient Linux Kernel 4.x Programming Techniques - Part 1

이번 posting에서는 아래 참고 문헌(References 내용)을 토대로 최신 kernel 4.x version에 적용 가능한 몇가지 linux kernel programming 기법을 소개 [첫번째 시간]하는 시간을 갖고자 한다. 아래 내용을 원할하게 이해하기 위해서는 (5년전에 작성한 내용이기는 하지만) 본 저자가 작성한 아래 파일 내용도 함께 참조해 주기 바란다.



<목차>
1. Target Board(UDOO Neo) 환경 소개
2. Kernel Timer 예제 소개
3. Interrupt & Tasklet 예제 소개
4. Threaded Interrupt 예제 소개
5. Interrupt & workqueue 예제 소개
6. Kernel Thread 예제 소개


1. Target Board(UDOO Neo) 환경 소개
Linux kernel 예제 program을 돌리기 위한 target board로는 UDOO Neo를 이용할 계획이고, kernel 및 rootfs 등은 buildroot를 통해 구성할 생각이다. 따라서 이번 절에서는 먼저 buildroot를 기반으로 UDOO Neo board를 부팅(NFS)하는 방법을 소개하고자 한다.

git clone git://git.buildroot.net/buildroot
  => buildroot 최신 버젼을 내려 받는다.

$ cd buildroot

make mx6sx_udoo_neo_defconfig
  => buildroot 최신 버젼에는 이미 udoo neo 보드용 config가 준비되어 있다.

make menuconfig
  => glibc를 enable하도록 수정한다. 추가로 원하는 내용이 있다면 반영하도록 한다.

make

<결과물>
chyi@earth:~/buildroot/output/images$ ls -la
합계 47556
drwxr-xr-x  3 chyi chyi     4096  2월 23 19:14 .
drwxrwxr-x  6 chyi chyi     4096  2월 22 11:51 ..
-rw-r--r--  1 chyi chyi    48128  2월 22 14:02 SPL
-rw-r--r--  1 chyi chyi    29277  2월 22 14:07 imx6sx-udoo-neo-basic.dtb
-rw-r--r--  1 chyi chyi    29098  2월 22 14:07 imx6sx-udoo-neo-extended.dtb
-rw-r--r--  1 chyi chyi    29277  2월 22 14:07 imx6sx-udoo-neo-full.dtb
-rw-r--r--  1 chyi chyi 15972352  2월 22 14:07 rootfs.ext2
lrwxrwxrwx  1 chyi chyi       11  2월 22 14:07 rootfs.ext4 -> rootfs.ext2
-rw-r--r--  1 chyi chyi 12410880  2월 22 14:07 rootfs.tar
-rw-r--r--  1 chyi chyi 17020928  2월 22 14:07 sdcard.img
-rwxr-xr-x  1 chyi chyi   237376  2월 22 14:02 u-boot.bin
-rw-r--r--  1 chyi chyi   237440  2월 22 14:02 u-boot.img
-rw-r--r--  1 chyi chyi  6173896  2월 22 14:07 zImage

cd output/images
mkdir rootfs; sudo tar xvf ./rootfs.tar -C ./rootfs
  => rootfs.tar의 압축을 푼다.

cp ./zImage /tftpboot
cp ./imx6sx-udoo-neo-full.dtb /tftpboot
  => NFS booting을 위해 zImage와 dtb 파일을 /tftpboot로 복사한다.

vi /etc/exports
...
~/IoT/UDOO/src/buildroot/output/images/rootfs 192.168.1.*(rw,no_root_squash,sync,no_subtree_check)
~
  => /etc/exports 파일에 buildroot rootfs의 path를 설정해 준다.

sudo /etc/init.d/nfs-kernel-server restart
  => nfs server를 재구동시킨다.

Buildroot가 정상적으로 build되었고, NFS booting을 위한 설정이 마무리 되었으니, 이제는 target board의 u-boot 설정을 변경하도록 하자.

<Target board - u-boot>
=> setenv ipaddr 192.168.1.50
=> setenv serverip 192.168.1.100
=> setenv netmask 255.255.255.0
=> ping 192.168.1.100
Using FEC0 device
host 192.168.1.100 is alive

=> setenv rootpath /home/chyi/IoT/UDOO/src/buildroot/output/images/rootfs
=> setenv nfsboot 'setenv bootargs root=/dev/nfs rw nfsroot=${serverip}:${rootpath} ip=${ipaddr}:${serverip}::${netmask}::eth0:off noinitrd console=ttymxc0,115200; tftp 0x80000000 zImage; tftp 0x86000000 imx6sx-udoo-neo-full.dtb; bootz 0x80000000 - 0x86000000'
  (*) 특이한 점은 noinitrd를 포함시켜야 한다는 점임.

=> run nfsboot


2.  Kernel Timer 예제 소개
이번 절에서는 몸풀기 단계로, kernel timer를 사용하는 간단한 예제를 소개해 보도록 하겠다. Kernel timer(혹은 dynamic timer)는 지정된 timer interval이 경과할 때가지 함수 실행을 지연시킬 필요가 있을 경우(쉽게 말해, 일정한 시간 간격을 두고 특정 함수를 실행)에 주로 사용된다.

2.1 Multiple Periodic Kernel Timers 예제(2개의 timer를 동시에 돌리는 예제)
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
<lab4_periodic_timers_alt.c>
/*
 * The code herein is: Copyright Jerry Cooperstein, 2012
 *
 * This Copyright is retained for the purpose of protecting free
 * redistribution of source.
 *
 *     URL:    http://www.coopj.com
 *     email:  coop@coopj.com
 *
 * The primary maintainer for this code is Jerry Cooperstein
 * The CONTRIBUTORS file (distributed with this
 * file) lists those known to have contributed to the source.
 *
 * This code is distributed under Version 2 of the GNU General Public
 * License, which you should have received with the source.
 *
 */

#include <linux/module.h>
#include <linux/timer.h>
#include <linux/init.h>
#include <linux/jiffies.h>
#include <linux/slab.h>

static struct my_data {   /* 아래 예제를 위해 만든 data structure - 한번에 두개의 timer를 돌리고자 함 */
unsigned long period;   /* timer function이 호출되는 주기 */
unsigned long start_time; /* timer 시작 시간: jiffies */
struct timer_list timer;  /* timer 선언include/linux/timer.h에 정의되어 있음 */
char struct_id; /* 두개의 timer를 구분하기 위한 id : 'A' or 'B' */
} *data_array; /* will kmalloc() an array of these */

static void my_timer_func(unsigned long var)   /* timer 기간 만료시 실행되는 함수 */
{
  struct my_data *dat = (struct my_data *)var;   /* 전달받은 argument를  struct my_data* type으로 casting */

pr_info("%c: period = %ld  elapsed = %ld\n", dat->struct_id, dat->period, jiffies - dat->start_time);
dat->start_time = jiffies;  /* timer 시작 시간 지정 */

mod_timer(&dat->timer, dat->period + jiffies);  /* timer가 만료되는 시간을 다시 초기화한다 */
}

static int __init my_init(void)  /* module을 시작한다 */
{
int i, period_in_secs;
struct my_data *d;

data_array = kmalloc(2 * sizeof(struct my_data), GFP_KERNEL);  /* struct my_data kernel memory 2개 할당 */

for (d = data_array, i = 0; i < 2; i++, d++) {
init_timer(&d->timer);  /* linked list의 previous, next pointer를 0으로 초기화 시킨다 */

period_in_secs = (i == 0) ? 1 : 10;
d->period = period_in_secs * HZ;  /* timer function이 호출되는 주기를 설정함. A timer: 1Hz, B timer: 10Hz로 지정 */
d->struct_id = 'A' + i;   /* A, B timer id */
d->start_time = jiffies;  /* timer 시작 시간 지정 */
d->timer.function = my_timer_func;  /* timer 만료 시 실행될 함수를 지정함 */
d->timer.expires = jiffies + d->period;  /* timer가 만료되는 시간 값을 지정함 - jiffie로 측정하는 절대 값임 */
d->timer.data = (unsigned long)d;  /* timer function에 전달할 argument를 지정함 */

add_timer(&d->timer);  /* global timer list에 새로운 timer를 추가함 */
}
pr_info("Module loaded, two timers started\n");
return 0;
}

static void __exit my_exit(void)  /* 모듈을 종료한다 */
{
int i;
struct my_data *d = data_array;
for (i = 0; i < 2; i++, d++) {
pr_info("deleted timer %c:  rc = %d\n", d->struct_id, del_timer(&d->timer));  /* timer 시간이 만료되기 전에 timer를 제거한다 */
}
kfree(data_array);  /* my_init()에서 할당했던  kernel memory를 해지한다 */
pr_info("Module unloaded\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_AUTHOR("Jerry Cooperstein");  /* 참고 문헌 [1]의 저자가 작성한 코드임 */
MODULE_AUTHOR("Bill Kerr");
MODULE_DESCRIPTION("LDD:2.0 s_11/lab4_periodic_timers_alt.c");
MODULE_LICENSE("GPL v2");
-------------------------------------------------------------------------------------------------------------------------------------------------------------------

참고로 아래 내용은 include/linux/timer.h에 정의되어 있는 struct timer_list의 실제 내용을 보여준다.

struct timer_list {
    /*
     * All fields that change during normal runtime grouped to the
     * same cacheline
     */
    struct hlist_node   entry;  /* struct hlist_node *next, **pprev를 담고 있음 */
    unsigned long       expires;
    void            (*function)(unsigned long);
    unsigned long       data;
    u32         flags;

#ifdef CONFIG_TIMER_STATS
    int         start_pid;
    void            *start_site;
    char            start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
    struct lockdep_map  lockdep_map;
#endif
};

-------------------------------------------------------------------------------------------------------------------------------------------------------------------
<Makefile>
obj-m +=  lab4_periodic_timers_alt.o
export KROOT=/home/chyi/IoT/UDOO/src/buildroot/output/build/linux-4.9  /* buildroot에 포함한 linux kernel 4.9의 위치 지정 */
export ARCH=arm

allofit:  modules
modules:
    @$(MAKE) -C $(KROOT) M=$(PWD) modules
modules_install:
    @$(MAKE) -C $(KROOT) M=$(PWD) modules_install
kernel_clean:
    @$(MAKE) -C $(KROOT) M=$(PWD) clean

clean: kernel_clean
    rm -rf   Module.symvers modules.order
-------------------------------------------------------------------------------------------------------------------------------------------------------------------

<How to compile>
$ export KROOT=/home/chyi/IoT/UDOO/src/buildroot/output/build/linux-4.9
$ export ARCH=arm
$ export CROSS_COMPILE=arm-linux-
$ export CC=arm-linux-gcc
$ export PATH=/home/chyi/IoT/UDOO/src/buildroot/output/host/usr/bin:$PATH
  => cross compile을 위한 환경 변수를 지정한다.

$ make
  => build하여 kernel module을 생성해 낸다.

$ sudo cp ./lab4_periodic_timers_alt.ko ~/IoT/UDOO/src/buildroot/output/images/rootfs/modules
  => build 결과물을 rootfs(NFS booting 용)의 적당한 위치로 복사한다.

<How to run it on the target board>
  => target board에서 실행한 결과는 다음과 같다.
# insmod ./lab4_periodic_timers_alt.ko 
[   33.083943] lab4_periodic_timers_alt: loading out-of-tree module taints kernel.
[   33.092944] Module loaded, two timers started
# [   34.171546] A: period = 100  elapsed = 108

# [   35.211081] A: period = 100  elapsed = 104
[   36.251044] A: period = 100  elapsed = 104
[   37.291056] A: period = 100  elapsed = 104
[   38.331037] A: period = 100  elapsed = 104
[   39.371047] A: period = 100  elapsed = 104
[   40.411067] A: period = 100  elapsed = 104
[   41.451044] A: period = 100  elapsed = 104
[   42.491065] A: period = 100  elapsed = 104
[   43.371035] B: period = 1000  elapsed = 1028
[   43.531028] A: period = 100  elapsed = 104
[   44.571067] A: period = 100  elapsed = 104
[   45.611060] A: period = 100  elapsed = 104
  => A, B 두개의 timer가 1HZ(100), 10HZ(10*100) 간격으로 호출되고 있음을 알 수 있다.

2.2 High resolution timer 예제
hrtimer는 nanosecond를 단위로 보다 세밀하게(정교하게) timer를 설정하는 방법으로 2.6.16 kernel 부터 도입되기 시작하였다. hrtimer는 CPU 당 1개의 list(timer 만료 시간을 기준으로 정렬된 list)가 존재하는 특징이 있다. hrtimer를 이해하기 위해서는 ktime_t(include/linux/ktime.h)를 살펴볼 필요가 있다.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
<lab5_hrtimer.c>
#include <linux/module.h>
#include <linux/kernel.h> /* for container_of */
#include <linux/timer.h>
#include <linux/init.h>
#include <linux/ktime.h>
#include <linux/hrtimer.h>
#include <linux/slab.h>

static struct my_data {  /* 아래 예제 코드를 위해 정의된 data structure */
ktime_t period;   /* timer interval */
int period_in_secs;
unsigned long start_time; /* timer 시작 시간: jiffies */
struct hrtimer timer;  /* hrtimer 변수 선언 */
char struct_id; /* 두개의 timer를 구분하기 위한 id 값 : 'A' or 'B' */
} *data_array; /* will kmalloc() an array of these */

static enum hrtimer_restart my_timer_func(struct hrtimer *var)  /* timer 만료시 호출되는 함수 */
{
struct my_data *dat = container_of(var, struct my_data, timer);  /* 넘어온 argument(var)와 timer element를 토대로 struct  my_data의 시작 주소를 역산한 후, type casting하여 struct my_data의 pointer를 얻어내는 함수 */
ktime_t now;

pr_info("%c: period = %d  elapsed = %ld\n", dat->struct_id, dat->period_in_secs, jiffies - dat->start_time);

now = var->base->get_time();  /* clock으로 부터 현재 시간을 얻어 옴 */
dat->start_time = jiffies;   /* timer 시작 시간 지정 : jiffies(현재 시간으로 보면 됨) */
hrtimer_forward(var, now, dat->period);   /* timer 만료 시간을 reset해 준다 */
return HRTIMER_RESTART;   /* timer 반복을 의미함. 한편, HRTIMER_NORESTART를 return할 경우에는, timer가 1회만 실행하고 멈추게 됨 */
}

static int __init my_init(void)
{
int i;
struct my_data *d;

data_array = kmalloc(2 * sizeof(struct my_data), GFP_KERNEL);  /* struct my_data 크기의 kernel 영역 2개 확보 */

for (d = data_array, i = 0; i < 2; i++, d++) {
d->period_in_secs = (i == 0) ? 1 : 10;  /* A timer 는 1, B timer는 10 선택 */
d->period = ktime_set(d->period_in_secs, 0);  /* seconds/nanoseconds 값으로 ktime_t 변수 설정함 */
d->struct_id = 'A' + i;  /* 2개의 timer 각각에 id 할당, 즉 A, B */

hrtimer_init(&d->timer, CLOCK_REALTIME, HRTIMER_MODE_REL);  /* time를 초기화한다. HRTIMER_MODE_REL:  now 시간에 대한 상대 시간을 의미함 */

d->timer.function = my_timer_func;  /* timer 함수 지정 */
d->start_time = jiffies;   /* timer 시작 시간 지정 : jiffies(현재 시간으로 보면 됨)*/

hrtimer_start(&d->timer, d->period, HRTIMER_MODE_REL);  /* current CPU 상에서 hrtimer를 시작한다 */
}
pr_info("Module loaded, two HRTimers started\n");

return 0;
}

static void __exit my_exit(void)
{
int i;
struct my_data *d = data_array;

for (i = 0; i < 2; i++, d++) {
pr_info("deleted timer %c:  rc = %d\n",
d->struct_id, hrtimer_cancel(&d->timer));  /* timer를 취소한다. 단, timer function이 종료되기를 기다린다 */
}
kfree(data_array);  /* my_init()에서 할당했던  kernel memory를 해지한다 */
pr_info("Module unloaded\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_AUTHOR("Jerry Cooperstein");
MODULE_AUTHOR("Bill Kerr");
MODULE_DESCRIPTION("LDD:2.0 s_11/lab5_hrtimer.c");
MODULE_LICENSE("GPL v2");
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
아래 내용은 include/linux/hrtimer.h에 정의되어 있는 struct hrtimer의 definition을 보여준다.

struct hrtimer {
    struct timerqueue_node      node;
    ktime_t             _softexpires;
    enum hrtimer_restart        (*function)(struct hrtimer *);
    struct hrtimer_clock_base   *base;
    u8              state;
    u8              is_rel;
#ifdef CONFIG_TIMER_STATS
    int             start_pid;
    void                *start_site;
    char                start_comm[16];
#endif
};

또한 아래 내용은 include/linux/ktime.h에 정의되어 있는 ktime_t의 실체를 보여주고 있다.

union ktime {
    s64 tv64;
};
 
typedef union ktime ktime_t;        /* Kill this */

<How to run it on the target board>
# insmod ./lab5_hrtimer.ko 
[ 1947.630129] Module loaded, two HRTimers started
# [ 1948.632834] A: period = 1  elapsed = 100
[ 1949.632833] A: period = 1  elapsed = 100
[ 1950.632835] A: period = 1  elapsed = 100
[ 1951.632832] A: period = 1  elapsed = 100
[ 1952.632852] A: period = 1  elapsed = 100
[ 1953.632842] A: period = 1  elapsed = 100
[ 1954.632835] A: period = 1  elapsed = 100
[ 1955.632842] A: period = 1  elapsed = 100
[ 1956.632807] A: period = 1  elapsed = 100
[ 1957.632817] A: period = 1  elapsed = 100
[ 1957.636846] B: period = 10  elapsed = 1000
[ 1958.632819] A: period = 1  elapsed = 100
[ 1959.632832] A: period = 1  elapsed = 100
[ 1960.632827] A: period = 1  elapsed = 100
[ 1961.632829] A: period = 1  elapsed = 100
[ 1962.632837] A: period = 1  elapsed = 100
[ 1963.632832] A: period = 1  elapsed = 100
  => 2.1의 예제와 비교할 때, 정확한 시간(100 or 1000)이 경과한 시점에 timer가 만료되고 있음을 알 수 있다.

3. Interrupt & Tasklet 예제 소개
이번 절에서는 UDOO Neo 보드 외부에 bread board를 하나 연결하고, 그 위에 switch 버튼을 장착한 상태에서 switch 버튼이 눌릴 경우(interrupt 발생), interrupt handler 내에서 tasklet이 동작하는 예제 program을 소개해 보고자 한다. Tasklet(상대적으로 개념이 모호한)은 bottom half의 한 종류로 softirq에 기반을 두고 있다. 대표적인 bottom half인 work queue와 비교하면 process context에서 실행되지 않으며, 동일한 CPU에서만 schedule이 진행되는 특징이 있어, cache coherency, serialization 등이 우수한 기법으로 볼 수  있다(간단히 bottom half를 만들 수 있는 편한 방법임).

그림 3.1 UDOO Neo 보드 interrupt 실험 환경 

그림 3.2 UDOO Neo 보드 external pinout 배치도

-------------------------------------------------------------------------------------------------------------------------------------------------------------------
<lab_one_interrupt_gpio.h>
#ifndef _LAB_ONE_INTERRUPT_H
#define _LAB_ONE INTERRUPT_H

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
#include <linux/kthread.h>
#include <linux/slab.h>

static int switch_gpio = 180;   /* 그림 3.2 우측 GPIO_180 pin 참조 */
static int irq = 0;
module_param(irq, int, S_IRUGO);

/* default delay time in top half -- try 10 to get results */
static int delay = 0;
module_param(delay, int, S_IRUGO);

static atomic_t counter_bh, counter_th;

struct my_dat {  /* interrupt handler에 넘기는 argument 형식으로 아래 예제에서는 크게 의미 없음 - 다른 예제와 연관이 있는 코드이므로 여기서는 무시하기로 함. 단 jiffies 변수는 사용함. */
unsigned long jiffies; /* used for timestamp */
struct tasklet_struct tsk; /* used in dynamic tasklet solution */
struct work_struct work; /* used in dynamic workqueue solution */
};
static struct my_dat my_data;

static irqreturn_t my_interrupt(int irq, void *dev_id);

static int __init my_generic_init(void)
{
atomic_set(&counter_bh, 0);
atomic_set(&counter_th, 0);

/* use my_data for dev_id */

if (request_irq(irq, my_interrupt, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                 "my_int", &my_data))   /* interrupt handler - my_interrupt() - 등록, RISING or FALLING edge 시 interrupt 발생
                                                        ...................... (A) */
{
pr_warning("Failed to reserve irq %d\n", irq);
return -1;
}
pr_info("successfully loaded\n");
return 0;
}

static void __exit my_generic_exit(void)
{
synchronize_irq(irq);
free_irq(irq, &my_data);  /* interrupt handler 등록 내용 제거 */
pr_info(" counter_th = %d,  counter_bh = %d\n", atomic_read(&counter_th), atomic_read(&counter_bh));
pr_info("successfully unloaded\n");
}

MODULE_AUTHOR("Jerry Cooperstein");
MODULE_AUTHOR("Chunghan Yi");
MODULE_DESCRIPTION("LDD:2.0 s_20/lab_one_interrupt.h");
MODULE_LICENSE("GPL v2");

#endif
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
#include <linux/module.h>
#include "lab_one_interrupt_gpio.h"  /* 위의 파일 */
#include <linux/gpio.h>

static atomic_t nevents; /* number of events to deal with */
static atomic_t catchup; /* number of 'missed events' */

static void t_fun(unsigned long t_arg)   /* tasklet 함수 */
{
struct my_dat *data = (struct my_dat *)t_arg;

/* did we get a spurious interrupt, or was it queued too late? */
if (atomic_read(&nevents) <= 0)   /* nevents 값이 0보다 작으면 바로 return 함 */
return;

for (;;) {
atomic_inc(&counter_bh);   /* counter_bh 값을 1 증가 시킴 */
pr_info
           ("In BH: counter_th = %d, counter_bh = %d, jiffies=%ld, %ld\n",
           atomic_read(&counter_th), atomic_read(&counter_bh),
           data->jiffies, jiffies);
if (atomic_dec_and_test(&nevents))   /* nevents 값을 1감소 시킨 후, 0이면 true를 return */
break;
atomic_inc(&catchup);   /* catchup 값을 1 증가 시킴 */
pr_info("****** nevents > 0, catchup=%d\n", atomic_read(&catchup));
}
}

/* initialize tasklet */
static DECLARE_TASKLET(t_name, t_fun, (unsigned long)&my_data);   /* tasklet 정적 초기화 - my_dat가 t_fun의 argument로 전달됨 */

static irqreturn_t my_interrupt(int irq, void *dev_id)   /* interrupt handler 함수, (A)에서 사용됨 */
{
struct my_dat *data = (struct my_dat *)dev_id;
  atomic_inc(&counter_th);   /* counter_th 값을 1 증가 시킴 */
atomic_inc(&nevents);  /* nevents 값을 1 증가 시킴 */
data->jiffies = jiffies;
tasklet_schedule(&t_name);   /* tasklet 수행 시작 요청 */
mdelay(delay); /* hoke up a delay to try to cause pileup */
return IRQ_NONE; /* we return IRQ_NONE because we are just observing */
}

static int __init my_init(void)
{
    int ret;

atomic_set(&catchup, 0);   /* catchup을 0으로 초기화 */
atomic_set(&nevents, 0);  /* nevents를 0으로 초기화 */

    /* switch_gpio 180(GPIO_180)번 pin을 gpio input으로 사용하겠다고 요청 */
ret = gpio_request_one(switch_gpio, GPIOF_IN, "test button");
if (ret < 0) {
pr_info("failed to request GPIO %d, error %d\n", switch_gpio, ret);
return -1;
}

#if 0  /* 위의 함수에서 GPIOF_IN 파라미터를 넘길 경우, 아래 함수 자동 실행됨 */
ret = gpio_direction_input(switch_gpio);
if (ret < 0) {
pr_info("failed to input GPIO %d, error %d\n", switch_gpio, ret);
return -1;
}
#endif

irq = gpio_to_irq(switch_gpio);  /* gpio line을 interrupt로 사용하도록 해 줌 */
if (irq < 0) {
pr_info("failed to gpio_to_irq() %d\n", irq);
return -1;
}

return my_generic_init();
}

static void __exit my_exit(void)
{
my_generic_exit();
gpio_free(switch_gpio);   /* gpio_request_one  한 내용 해제 */
pr_info("Final statistics:   catchup = %d\n", atomic_read(&catchup));
}

module_init(my_init);
module_exit(my_exit);

MODULE_AUTHOR("Jerry Cooperstein");
MODULE_AUTHOR("Chunghan Yi");
MODULE_DESCRIPTION("LDD:2.0 s_20/lab3_one_tasklet_improved.c");
MODULE_LICENSE("GPL v2");
-------------------------------------------------------------------------------------------------------------------------------------------------------------------

<How to run it on the target board>
# insmod ./lab3_one_tasklet_improved_gpio.ko
[ 9147.234166] gpio_request_one() ok - switch_gpio: 180 !
[ 9147.239687] gpio_to_irq() ok - irq : 238 !
[ 9147.244176] successfully loaded
#
  => button을 누르거나 뗄 경우, 아래 메시지 출력(interrupt 발생 후, tasklet 수행)
# [ 9150.091393] In BH: counter_th = 4, counter_bh = 1, jiffies=885008, 885008
[ 9150.098384] ****** nevents > 0, catchup=1
[ 9150.102458] In BH: counter_th = 4, counter_bh = 2, jiffies=885008, 885009
[ 9150.109409] ****** nevents > 0, catchup=2
[ 9150.113482] In BH: counter_th = 4, counter_bh = 3, jiffies=885008, 885010
[ 9150.120396] ****** nevents > 0, catchup=3
[ 9150.124465] In BH: counter_th = 4, counter_bh = 4, jiffies=885008, 885011
[ 9150.983455] In BH: counter_th = 7, counter_bh = 5, jiffies=885097, 885097
[ 9150.990360] ****** nevents > 0, catchup=4
[ 9150.994396] In BH: counter_th = 10, counter_bh = 6, jiffies=885097, 885097
[ 9151.001288] ****** nevents > 0, catchup=5
[ 9151.005317] In BH: counter_th = 10, counter_bh = 7, jiffies=885097, 885097
[ 9151.012207] ****** nevents > 0, catchup=6
[ 9151.016235] In BH: counter_th = 10, counter_bh = 8, jiffies=885097, 885097
[ 9151.023124] ****** nevents > 0, catchup=7
[ 9151.027151] In BH: counter_th = 10, counter_bh = 9, jiffies=885097, 885097
[ 9151.034039] ****** nevents > 0, catchup=8
[ 9151.038123] In BH: counter_th = 10, counter_bh = 10, jiffies=885097, 885103
[ 9151.363217] In BH: counter_th = 11, counter_bh = 11, jiffies=885135, 885135


그림 3.3 my_int interrupt handler가 등록된 모습 확인(irq: 238)



4. Threaded Interrupt 예제 소개
이번 절에서는 interrupt 발생 시, 자동으로 kernel thread가 하나 생성된 뒤, 이 곳에서 interrupt 요청을 처리하는 방식인 threaded interrupt 기법에 대해 소개하고자 한다. 실험에 사용된 환경은 그림 3.1과 동일하다.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------
<lab_one_interrupt_gpio_thread.h>
#ifndef _LAB_ONE_INTERRUPT_H
#define _LAB_ONE INTERRUPT_H

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
#include <linux/kthread.h>
#include <linux/slab.h>

static int switch_gpio = 180;    /* 그림 3.2 우측 GPIO_180 pin 참조 */
static int irq = 0;
module_param(irq, int, S_IRUGO);

/* default delay time in top half -- try 10 to get results */
static int delay = 0;
module_param(delay, int, S_IRUGO);

static atomic_t counter_bh, counter_th;

struct my_dat {   /* interrupt handler에 넘기는 argument 형식으로 아래 예제에서는 크게 의미 없음 */
unsigned long jiffies; /* used for timestamp */
struct tasklet_struct tsk; /* used in dynamic tasklet solution */
struct work_struct work; /* used in dynamic workqueue solution */
};
static struct my_dat my_data;

static irqreturn_t my_interrupt(int irq, void *dev_id);
static irqreturn_t thread_fun(int irq, void *thr_arg);

static int __init my_generic_init(void)
{
atomic_set(&counter_bh, 0);
atomic_set(&counter_th, 0);

/* use my_data for dev_id */

if (request_threaded_irq(irq, my_interrupt, thread_fun, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"my_int", &my_data))   /* my_interrupt handler 내에서  IRQ_WAKE_THREAD를 return할 경우, thread_fun가 thread로 실행됨 */
{
pr_warning("Failed to reserve irq %d\n", irq);
return -1;
}
pr_info("successfully loaded\n");
return 0;
}

static void __exit my_generic_exit(void)
{
  synchronize_irq(irq);
free_irq(irq, &my_data);
pr_info(" counter_th = %d,  counter_bh = %d\n", atomic_read(&counter_th), atomic_read(&counter_bh));
pr_info("successfully unloaded\n");
}

MODULE_AUTHOR("Jerry Cooperstein");
MODULE_AUTHOR("Chunghan Yi");
MODULE_DESCRIPTION("LDD:2.0 s_20/lab_one_interrupt.h");
MODULE_LICENSE("GPL v2");


#endif
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
<lab6_one_threaded_gpio.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/gpio.h>

#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,30)
static int __init my_init(void)
{
pr_warning("Threaded interrupts don't appear until 2.6.30\n");
return -1;
}

module_init(my_init)
#else

#define THREADED_IRQ

#include "lab_one_interrupt_gpio_thread.h"    /* 위의 파일 */

static atomic_t nevents;

static irqreturn_t my_interrupt(int irq, void *dev_id)    /* interrupt handler 함수 */
{
atomic_inc(&counter_th);   /* counter_th 1 증가 시킴 */
atomic_inc(&nevents);   /* nevents 1 증가 시킴 */
mdelay(delay); /* hoke up a delay to try to cause pileup */
return IRQ_WAKE_THREAD;   /* 이 값을 return할 경우, handler thread를 깨우고 thread_fun을 실행하게 됨 */
}

static irqreturn_t thread_fun(int irq, void *thr_arg)
{
do {
atomic_inc(&counter_bh);   /* counter_bh 1 증가 시킴 */
}
while (!atomic_dec_and_test(&nevents));   /* nevents를 1 감소 후, 그 값이 0이면 true return ! */
pr_info("In BH: counter_th = %d, counter_bh = %d, nevents=%d\n",
atomic_read(&counter_th), atomic_read(&counter_bh),
atomic_read(&nevents));

/* we return IRQ_NONE because we are just observing */
return IRQ_NONE;
}

static int __init my_init(void)
{
int ret;

atomic_set(&nevents, 0);  /* nevents을 0으로 초기화 */

   /* switch_gpio 180(GPIO_180)번 pin을 gpio input으로 사용하겠다고 요청 */
ret = gpio_request_one(switch_gpio, GPIOF_IN, "test button");
if (ret < 0) {
pr_info("failed to request GPIO %d, error %d\n", switch_gpio, ret);
return -1;
}

#if 0
ret = gpio_direction_input(switch_gpio);
if (ret < 0) {
pr_info("failed to input GPIO %d, error %d\n", switch_gpio, ret);
return -1;
}
#endif

irq = gpio_to_irq(switch_gpio);
if (irq < 0) {
pr_info("failed to gpio_to_irq() %d\n", irq);
return -1;
}

return my_generic_init();
}

static void __exit my_exit(void)
{
    my_generic_exit();
    gpio_free(switch_gpio);
}

module_init(my_init);
module_exit(my_exit);
#endif

MODULE_AUTHOR("Jerry Cooperstein");
MODULE_AUTHOR("Chunghan Yi");
MODULE_DESCRIPTION("LDD:2.0 s_20/lab6_one_threaded.c");

MODULE_LICENSE("GPL v2");
-------------------------------------------------------------------------------------------------------------------------------------------------------------------

# insmod ./lab6_one_threaded_gpio.ko 
[ 2362.187954] successfully loaded
  => switch 버튼을 누르거나 뗄 경우, 아래 메시지(thread_fun 함수에서 아래 내용 출력)가 출력됨.
# [ 2401.757974] In BH: counter_th = 1, counter_bh = 1, nevents=0
[ 2405.693092] In BH: counter_th = 2, counter_bh = 2, nevents=0
[ 2405.856909] In BH: counter_th = 3, counter_bh = 3, nevents=0
[ 2405.882451] In BH: counter_th = 4, counter_bh = 4, nevents=0
[ 2405.897686] In BH: counter_th = 5, counter_bh = 5, nevents=0
[ 2405.903620] In BH: counter_th = 6, counter_bh = 6, nevents=0
[ 2405.909460] In BH: counter_th = 7, counter_bh = 7, nevents=0
[ 2406.072081] In BH: counter_th = 13, counter_bh = 13, nevents=0
[ 2406.078320] In BH: counter_th = 21, counter_bh = 21, nevents=0
[ 2406.084350] In BH: counter_th = 25, counter_bh = 25, nevents=0
[ 2406.090471] In BH: counter_th = 28, counter_bh = 28, nevents=0
[ 2406.096469] In BH: counter_th = 31, counter_bh = 31, nevents=0
[ 2406.102473] In BH: counter_th = 32, counter_bh = 32, nevents=0
[ 2406.108475] In BH: counter_th = 33, counter_bh = 33, nevents=0
[ 2406.135438] In BH: counter_th = 34, counter_bh = 34, nevents=0
[ 2406.285774] In BH: counter_th = 35, counter_bh = 35, nevents=0
[ 2406.291930] In BH: counter_th = 36, counter_bh = 36, nevents=0
[ 2406.298301] In BH: counter_th = 37, counter_bh = 37, nevents=0
[ 2406.304258] In BH: counter_th = 38, counter_bh = 38, nevents=0
[ 2406.310273] In BH: counter_th = 39, counter_bh = 39, nevents=0
[ 2406.316193] In BH: counter_th = 40, counter_bh = 40, nevents=0
[ 2406.322189] In BH: counter_th = 41, counter_bh = 41, nevents=0

아래 그림 4.1은, 위의 예제 실행 시 자동으로 구동된 kernel thread(irq/238-my_int)의 위엄(?)을 보여준다.

그림 4.1 threaded irq 실행 예(irq/238-my_int thread)


5. Interrupt & workqueue 예제 소개
이번 절에서는 동일한 실험 환경(그림 3.1)에서 interrupt 발생 시, workqueue를 통해 bottom half 처리를 하는 예제를 소개해 보도록 하겠다. 반복되는 코드(lab_one_interrupt_gpio.h)는 생략하고, workqueue 코드 중심으로 살펴 보기로 하자.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------
<lab3_one_workqueue_dynamic_gpio.c>
#include <linux/module.h>
#include "lab_one_interrupt_gpio.h"
#include <linux/gpio.h>

static void w_fun(struct work_struct *w_arg)   /* work queue 함수 */
{
struct my_dat *data = container_of(w_arg, struct my_dat, work);  /* argument로 넘어온 w_arg와 struct my_dat의 element인 work를 토대로 struct my_dat structure를 복원 후, type casting해 줌 - structure의 시작 부터 work element까지의 차를 계산 후, 이 값을 뺀 위치를 시작 위치로 산정 ! */

atomic_inc(&counter_bh);   /* counter_bh 값을 1 증가 시킴 */
pr_info("In BH: counter_th = %d, counter_bh = %d, jiffies=%ld, %ld\n",
atomic_read(&counter_th), atomic_read(&counter_bh),
data->jiffies, jiffies);
kfree(data);
}

static irqreturn_t my_interrupt(int irq, void *dev_id)   /* interrupt handler 함수 */
{
  struct my_dat *data =
        (struct my_dat *)kmalloc(sizeof(struct my_dat), GFP_ATOMIC);
data->jiffies = jiffies;

INIT_WORK(&data->work, w_fun);   /* work queue 초기화 - 정적 방식 */
atomic_inc(&counter_th);  /* counter_th 값을 1 증가 시킴 */
schedule_work(&data->work);   /* work queue 함수가 실행되도록 schedule 함 - 위의 w_fun() 함수가 실행되도록 해 줌 */
mdelay(delay); /* hoke up a delay to try to cause pileup */
return IRQ_NONE; /* we return IRQ_NONE because we are just observing */
}

static int __init my_init(void)
{
int ret;

ret = gpio_request_one(switch_gpio, GPIOF_IN, "test button");
if (ret < 0) {
pr_info("failed to request GPIO %d, error %d\n", switch_gpio, ret);
return -1;
}

irq = gpio_to_irq(switch_gpio);
if (irq < 0) {
pr_info("failed to gpio_to_irq() %d\n", irq);
return -1;
}

return my_generic_init();
}

static void __exit my_exit(void)
{
    cancel_work_sync(&my_data.work);   /* work을 취소하고 끝날때까지 기다린다 */
my_generic_exit();
gpio_free(switch_gpio);
}

module_init(my_init);
module_exit(my_exit);

MODULE_AUTHOR("Jerry Cooperstein");
MODULE_AUTHOR("Chunghan Yi");
MODULE_DESCRIPTION("LDD:2.0 s_20/lab3_one_workqueue_dynamic_gpio.c");
MODULE_LICENSE("GPL v2");
-------------------------------------------------------------------------------------------------------------------------------------------------------------------

# insmod ./lab3_one_workqueue_dynamic_gpio.ko
[ 5928.025869] successfully loaded
  => switch 버튼을 누르거나 뗄 경우, 아래 메시지(w_fun 함수에서 아래 내용 출력)가 출력됨.
# [ 5934.883333] In BH: counter_th = 1, counter_bh = 1, jiffies=563487, 563487
[ 5944.005303] In BH: counter_th = 2, counter_bh = 2, jiffies=564399, 564399
[ 5944.080938] In BH: counter_th = 32, counter_bh = 3, jiffies=564407, 564407
[ 5944.088429] In BH: counter_th = 41, counter_bh = 4, jiffies=564407, 564408
[ 5944.095595] In BH: counter_th = 48, counter_bh = 5, jiffies=564407, 564408
[ 5944.104541] In BH: counter_th = 50, counter_bh = 6, jiffies=564407, 564409
[ 5944.111776] In BH: counter_th = 51, counter_bh = 7, jiffies=564407, 564410
[ 5944.120192] In BH: counter_th = 51, counter_bh = 8, jiffies=564407, 564411
[ 5944.127188] In BH: counter_th = 51, counter_bh = 9, jiffies=564407, 564411
[ 5944.135571] In BH: counter_th = 51, counter_bh = 10, jiffies=564407, 564412
[ 5944.142788] In BH: counter_th = 51, counter_bh = 11, jiffies=564407, 564413
[ 5944.151205] In BH: counter_th = 51, counter_bh = 12, jiffies=564407, 564414
[ 5944.158438] In BH: counter_th = 51, counter_bh = 13, jiffies=564407, 564415
[ 5944.165497] In BH: counter_th = 51, counter_bh = 14, jiffies=564407, 564415
[ 5944.174239] In BH: counter_th = 51, counter_bh = 15, jiffies=564407, 564416
[ 5944.181540] In BH: counter_th = 52, counter_bh = 16, jiffies=564407, 564417
[ 5944.189981] In BH: counter_th = 52, counter_bh = 17, jiffies=564407, 564418
[ 5944.197058] In BH: counter_th = 52, counter_bh = 18, jiffies=564407, 564418
[ 5944.205487] In BH: counter_th = 52, counter_bh = 19, jiffies=564407, 564419
[ 5944.212698] In BH: counter_th = 52, counter_bh = 20, jiffies=564407, 564420

참고로, (앞의 경우이기는 하지만)workqueue에서는 별도의 kernel thread를 생성하는 것이 아니라 (이미 동작 중인)kworker thread를 통해 work을 수행함을 알 수 있다.


6. Kernel Thread 예제 소개
이번 절에서는 모듈 시작 시 kernel thread를 하나 생성해 둔 상태에서 interrupt 발생 시, 해당 thread가 의미 있는 작업을 수행하도록 하는 예제 program을 소개해 보도록 하겠다. 실험에 사용한 환경은 앞의 예제들과 동일하다. Kernel thread 예제는 앞서 4절의 threaded interrupt나 5절의 work queue 예제와 내부적으로 처리되는 방식이 크게 다르지 않지만, 실제 kernel thread code를 작성해 봄으로써 추후 이를 활용할 경우를 대비해 볼 수 있으리라 여겨진다. Kernel thread는 kernel 내에서 반복적으로 어떤 일을 수행하고자 할 때 만들어 두면 편리하게 사용할 수 있다.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------
<lab3_one_thread_improved_gpio.c>
#include <linux/module.h>
#include "lab_one_interrupt_gpio.h"
#include <linux/gpio.h>

static atomic_t cond;   /* thread 내에서 sleep 상태를 벋어나는 추가 조건으로 사용하는 변수 */
static atomic_t nevents; /* number of events to deal with */
static atomic_t catchup; /* number of 'missed events' */

static DECLARE_WAIT_QUEUE_HEAD(wq);   /* wait queue 정적 초기화, wait queue는 kernel mode에서 running중인 task가 특정 조건이 만족될 때까지 기다려야 할 때 주로 사용된다. */

static struct task_struct *tsk;   /* task pointer 선언 */

static irqreturn_t my_interrupt(int irq, void *dev_id)   /* interrupt handler 함수 */
{
struct my_dat *data = (struct my_dat *)dev_id;
atomic_inc(&nevents);   /* nevents 값을 1 증가 시킴 */
atomic_inc(&counter_th);  /* counter_th의 값을 1 증가 시킴 */
atomic_set(&cond, 1);   /* interrupt 발생 시마다, cond 값을 1로 초기화 */
data->jiffies = jiffies;
mdelay(delay); /* hoke up a delay to try to cause pileup */
wake_up_interruptible(&wq);   /* 잠자고 있는 task(여기서는 thread)를 깨운다. */
return IRQ_NONE; /* we return IRQ_NONE because we are just observing */
}

static int thr_fun(void *thr_arg)   /* thread 함수 */
{
struct my_dat *data;
data = (struct my_dat *)thr_arg;

/* go into a loop and deal with events as they come */

do {   /* 아래 do while loop은 thread 강제 종료 등의 상황이 아니면 무한 반복하게 됨 */
atomic_set(&cond, 0);  /* cond 값을 0으로 설정함 */
wait_event_interruptible(wq, kthread_should_stop()
|| atomic_read(&cond));   /* wait queue wq에 대해 wake_up_X() 함수가 호출될 때가지 sleep 상태 유지, wake_up_X() 함수가 호출되면 sleep에서 깨어나 아래 코드 수행하게 됨. 주의: 이때 cond 값이 1(interrupt 발생을 의미)인지도 함께 확인함. */
/* did we get a spurious interrupt, or was it queued too late? */
if (kthread_should_stop())   /* thread를 멈춰야 하는 조건인지 체크 */
return 0;
if (atomic_read(&nevents) <= 0)   /* nevents 값이 0보다 작으면 do while loop 계속 */
continue;
for (;;) {
atomic_inc(&counter_bh);   /* counter_bh 값 1 증가 시킴 */
pr_info
   ("In BH: counter_th = %d, counter_bh = %d, jiffies=%ld, %ld\n",
    atomic_read(&counter_th), atomic_read(&counter_bh),
    data->jiffies, jiffies);
if (atomic_dec_and_test(&nevents))  /* nevents 값을 1감소 시킨 후, 0이면 true return */
break;
atomic_inc(&catchup);   /* catchup 값을 1 증가 시킴 */
pr_info("****** nevents > 0, catchup=%d\n",
atomic_read(&catchup));
}
} while (!kthread_should_stop());    /* thread를 멈춰야 하는 조건(얘: thread kill 상황)인지 체크 */
return 0;
}

static int __init my_init(void)
{
int ret;

atomic_set(&cond, 1);   /* cond 값을 1로 설정 */
atomic_set(&catchup, 0);  /* catchup 값을 0으로 설정 */
atomic_set(&nevents, 0);  /* nevemts 값을 0으로 설정 */
if (!(tsk = kthread_run(thr_fun, (void *)&my_data, "thr_fun"))) {   /* thread를 하나 만든다. thread의 이름은 thr_fun임, my_data가 thr_fun() 함수의 argument로 넘어간다. */
pr_info("Failed to generate a kernel thread\n");
return -1;
}

ret = gpio_request_one(switch_gpio, GPIOF_IN, "test button");
if (ret < 0) {
pr_info("failed to request GPIO %d, error %d\n", switch_gpio, ret);
return -1;
}

irq = gpio_to_irq(switch_gpio);
if (irq < 0) {
pr_info("failed to gpio_to_irq() %d\n", irq);
return -1;
}

return my_generic_init();
}

static void __exit my_exit(void)
{
kthread_stop(tsk);
my_generic_exit();
gpio_free(switch_gpio);
pr_info("Final statistics:   catchup = %d\n", atomic_read(&catchup));
}

module_init(my_init);
module_exit(my_exit);

MODULE_AUTHOR("Jerry Cooperstein");
MODULE_AUTHOR("Chunghan Yi");
MODULE_DESCRIPTION("LDD:2.0 s_20/lab3_one_thread_improved_gpio.c");
MODULE_LICENSE("GPL v2");
-------------------------------------------------------------------------------------------------------------------------------------------------------------------

# insmod ./lab3_one_thread_improved_gpio.ko
[ 1473.499847] successfully loaded
=> switch 버튼을 누르거나 뗄 경우, 아래 메시지(thr_fun 함수에서 출력)가 출력됨.
# [ 1497.662408] In BH: counter_th = 1, counter_bh = 1, jiffies=119765, 119765
[ 1498.920894] In BH: counter_th = 3, counter_bh = 2, jiffies=119891, 119891
[ 1498.927801] ****** nevents > 0, catchup=1
[ 1498.932084] In BH: counter_th = 5, counter_bh = 3, jiffies=119892, 119892
[ 1498.939051] ****** nevents > 0, catchup=2
[ 1498.943148] In BH: counter_th = 7, counter_bh = 4, jiffies=119893, 119893
[ 1498.950097] ****** nevents > 0, catchup=3
[ 1498.954192] In BH: counter_th = 9, counter_bh = 5, jiffies=119894, 119894
[ 1498.961113] ****** nevents > 0, catchup=4
[ 1498.965179] In BH: counter_th = 9, counter_bh = 6, jiffies=119894, 119895
[ 1498.972120] ****** nevents > 0, catchup=5
[ 1498.976185] In BH: counter_th = 10, counter_bh = 7, jiffies=119896, 119896
[ 1498.983185] ****** nevents > 0, catchup=6
[ 1498.987276] In BH: counter_th = 11, counter_bh = 8, jiffies=119897, 119897
[ 1498.994352] ****** nevents > 0, catchup=7
[ 1498.998527] In BH: counter_th = 14, counter_bh = 9, jiffies=119899, 119899
[ 1499.005475] ****** nevents > 0, catchup=8
[ 1499.009760] In BH: counter_th = 23, counter_bh = 10, jiffies=119900, 119900
[ 1499.016855] ****** nevents > 0, catchup=9
[ 1499.021007] In BH: counter_th = 26, counter_bh = 11, jiffies=119900, 119901
[ 1499.028017] ****** nevents > 0, catchup=10
[ 1499.032296] In BH: counter_th = 26, counter_bh = 12, jiffies=119900, 119902
[ 1499.039408] ****** nevents > 0, catchup=11
[ 1499.043562] In BH: counter_th = 26, counter_bh = 13, jiffies=119900, 119903
[ 1499.050742] ****** nevents > 0, catchup=12

그림 6.1 kernel thread [thr_fun] 실행 모습

이상으로 Linux kernel 4.x에서도 동작 가능한, 몇가지 kernel programming 기법을 예제를 통해 확인해 보았다. 참고로  지금까지 소개한 코드는 아래 github에서 확인할 수 있다.



References
1. Writing Linux Device Drivers(a guide with exercises), Jerry Cooperstein.
2. Linux Device Drivers 3rd Edition, Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman.
3. Essential Linux Device Drivers, Sreekrishnan Venkateswaran.
4. Linux Kernel Development 3rd Edition, Robert Love.
5. http://kernel.readthedocs.io/en/sphinx-samples


Slowboot




댓글 없음:

댓글 쓰기