라이브러리 로딩 ld.so.conf



  라이브러리란 프로그램들이 공통으로 사용할 수 있는 기능을 포함하고 있는 오브젝트 파일입니다.  


  동적 라이브러리는 프로그램을 컴파일하여 생성되는 바이너리에 포함하지 않고 바이너리가 실행하는 시점 또는 실행 후에 포함시킬 수 있도록 제작된 라이브러리를 말합니다. 그래서 동적이라는 이름이 붙게 된 것입니다. 이런 형ㅌ식의 라이브러리는 프로그램을 실행할 때 호출되는 로더에 의해서 메모리에 적재됩니다.


  동적 라이브러리를 호출하기 위해서는 path 지정이 필수적입니다. 해당 라이브러리가 어디에 위치해 있는지 모든 디렉토리를 탐색하고 로드하기에는 비효율적이기 때문입니다. 우리가 흔히 설정하는 LD_LIBRARY_PATH 환경변수가 동적 라이브러리 호출을 위한 path 지정에 사용되는 환경 변수이며 또 다른 방법으로는 시스템 설정을 통해서 지정할 수 있습니다. 시트템 설정을 위한 설정 파일이 리눅스는 /etc/ld.so,conf 파일 입니다. Kernel 버전이 2.6 이상부터는 /etc/ld.so.conf.d/ 디렉토레 내에 *.conf 파일 형식으로 여려 파일을 통해 설정을 할 수 있습니다.


  로더는 LD_LIBRARY_PATH 또는 ld.so.conf에 명시된 디렉토리 내에서 동적 라이브러리를 찾으며 해당 디렉토리에 없으면, Not Found 에러를 출력합니다. 컴파일 시 Link 오류나 실행시 Loading 오류가 발생한다면 우선적으로 두가지 설정을 확인해 봐야 합니다.


  LD_LIBRARY_PATH 나 ld.so.conf 두 곳 모두 설정할 필요는 없으나 다음과 같은 경우에는 ld.so.conf 에 설정하도록 유의해야 합니다. 바이너리에 setuid, setgid 등이 설정된 경우 리눅스의 로더는 LD_LIBRARY_PATH 환경 변수 설정을 무시해 버린다고 합니다. 그도 그럴것이 악의적으로 시스템 주요 함수들이 setuid를 가진 바이너리가 호출한 라이브러리에 의해서 오버라이딩 된다면 시스템에 엄청난 일을 가져다 줄 것 입니다.(프로세스 등의 문제들, 해킹) 그런 보안을 고려한 것인지 모르겠지만 로더는 ( uid != euid || gid != egid ) 상황에서 LD_LIBRARY_PATH 를 무시하기 때문에 아무리 컴파일시에 link가 정상이고 ldd로 확인해도 정상이지만 바이너리 실행시 해당 라이브러리를 찾을 수 없다는 오류를 만나게 됩니다. 이런 경우는 LD_LIBRARY_PATH가 아니라 ld.so.conf 에 설정을 해줘야 합니다.

 


관련 명령어 :

  • ldconfig - /etc/ld.so.conf 설정된 동적 라이브러리 정보를 /etc/ld.so.cache 파일로 만들어 주는 일을 한다. 이로서 로더는 ld.so.cache 정보를 기반으로 보다 빠르게 라이브러리를 찾아 낼 수 가 있다. ld.so.conf 설정을 변경하면 반드시 ldconfig 명령을 수행하여 cache 를 갱신해 주도록 하자.
  • ldconfig
    ldconfig -p

  • ldd - 공유 라이브러리의 의존성을 검사해 준다. 동적 라이브러리도 공유 라이브러리 이므로 link 시나 load 시에 에러가 난다면 점검을 해보자.
  • ldd <binary name>

  •  objdump - 오브젝트 파일에 대한 정보를 출력해 준다. Dynamic Section의 NEEDED 로 표기된 항목이 공유 라이브러리를 표시한다고 한다.
  • objdump -p <object(binary) name>


관련 링크 :  




'System Programming > Linux Kernel' 카테고리의 다른 글

udev  (0) 2016.09.12
커널 타이머  (0) 2016.06.08
[Linux] ticket spin lock  (0) 2016.05.17
spin_lock, spin_lock_irq, spin_lock_irqsave  (2) 2016.05.17

* 시스템 리소스의 표현

- 시스템 리소스를 파일로 표현 및 제공하는 것을 의미

- 시스템에 있는 H/W는 물론, 논리적인 시스템 리소스 전반을 파일로 표현

- /proc, /dev, /sys 내에 디바이스 파일이나 프로세스와 관련된 파일, 시스템 리소스를 표현하는 파일 위치

- /dev/sda 파일은 하드디스크 추상화 파일


* 추상화된 파일의 역할

- Application 을 위한 인터페이스 도구

- 리눅스는 사용자 공간과 커널 공간이 가상주소를 기반으로 분리된 시스템 구조

- 이는 Application 이나 커널은 서로 직접적인 접근이 불가능

- Application 이 커널의 리소스를 사용하기 위해서는 S/W 인터럽트인 system call 로 커널의 작업 수행을 요청 하는 구조를 제공


###  udev 란 ?


* 탄생배경

- /dev 밑에 디바이스 파일 노드를 만드는 데 어려움

- RedHat 9 버전인 경우 /dev/에는 18,000 개의 디바이스 노드 파일 존재

- 이후에 생성될 가능성이 있는 모든 디바이스에 대한 모든 노드 파일을 미리 생성


*udev란

- A Userspace Implementation of devfs 의 약자

- 실제로 있는 디바이스에 대한 파일 노드 생성을 사용자 공간에서 자동화하여 처리하도록 구성

- /dev 디렉토리에 장치 노드 파일을 만들거나 제거하는 작업을 동적으로 수행

- 안드로이드에서는 시스템이 부팅될 때 두 개의 데몬이 동작

- udev 와 uevent

- uevent 데몬은 특정 디바이스가 hotplug 되는 경우 커널 영역에서는 모듈과 같은 형태의 device driver 가 로딩되며, 

이 때 해당 디바이스에 대한 정보가 uevent 로 udev 데몬에 전달하며 디바이스 노드 생성시 사용되도록 /sys 디렉토리

내부에 대항 디바이스에 대한 uevent 파일을 생성

- udev 데몬은 수신된 uevent 데이터와 /sys/class/device_name/uevent 파일을 비교하여 /dev 디렉토리 아래에 디바이스

노드 생성 여부를 결정 및 수행


### udev 의 특징


* hotplug

- udev 는 hotplug 서브시스템의 일부로 장치가 시스템에 추가되거나 제거되면 hotplug가 udev를 호출

- udev 는 hotplug 이벤트를 받아 sysfs 에서 필요한 정보를 얻어 /dev 아래에 장치 파일 생성


- 리눅스 부팅 시 init 의 실행 시 /sbin/init 프로세스가 /etc/inittab 로딩 후 각 스크립트 등을 실행

- 이 때 sysfs 를 /sys 로 마운트하여 udev 데몬이 동작

- udev 데몬이 실행 된 후 시스템에서 디바이스 노드 생성

- 실제 디바이스가 detect 되면 sysfs 에 등록되고 해당 디바이스는 사용자 공간에서 /sys에 등록

- udev 데몬으로 netlink socket 을 이용하여 새로운 디바이스가 생성되었다는 메시지를 전송

- udev 데몬은 /sys의 디바이스 내용 (Major, Minor 번호 및 생성할 디바이스 파일의 이름 등) 을

이용해서 /dev 에 디바이스 노드를 생성


### udeventd


* Android 에서의 디바이스 관리 방법

- udev 및 uevent 에 대한 내용은 거의 동일

- 시스템 특성상 이를 변형하여 사용

- Froyo 버전 까지 init 프로세스에 의해서 전반적인 시스템 메니지먼트를 이룸

- Gingerbrad 버전부터는 init 프로세스에서 처리하던 디바이스의 관리를 udeventd 데몬으로 분리




./system/core/init/init.c

 

int main(int argc, char **argv)

{

    ....

    

    if (!strcmp(basename(argv[0]), "ueventd"))

        return ueventd_main(argc, argv);

    ....


}

- init 프로세스의 main 함수는 시작 부분에 ueventd_main 함수를 호출하여 ueventd 데몬 실행

- ueventd 데몬이나 udevd 데몬은 시스템이 초기화되는 시점에 동작하며 /dev 디렉토리 아래에 디바이스 파일을 동적으로 생성


* ueventd 데몬의 동작 구조




ueventd.%hardware%.rc

Nexus Sdml /ueventd.herring.rc 내용

 


/dev/pvrsrvkm            0666    system    system

/dev/uwibro               0660    system    system

/dev/swmxctl             0660    system    system

/dev/video0               0660    system    camera

/dev/video1               0660    system    camera

/dev/video2               0660    system    camera

/dev/s3c-jpg             0660    system    camera

/dev/s3c-mem           0660    system    system

/dev/s3c-mfc            0660     media      media


/dev/modem_ctl          0660     radio       radio

/dev/modem_fmt          0660     radio       radio

/dev/modem_rfs          0660     radio       radio

/dev/s3c2410_serial3   0660     radio       radio

/dev/block/mtdblcok5  0660     radio       radio

/dev/mtd/mtd5ro         0660     radio       radio

/dev/mtd/mtd5            0660     radio       radio


# for Sensor HAL

/dev/akm 8973            0660    system    system

/dev/accelerometer     0660    system    system


# for GPS

/dev/s3c2410_serial1    0600     gps        gps 



- ueventd 데몬은 위 내용 대로 디바이스 파일 생성

- 시스템 부팅 과정에서 기본적으로 /dev 로 마운트하여 사용하는 tmpfs의 내용 이외에 ueventd 데몬에 의해서 관리 될 시스템 디바이스

파일을 의미




./system/core/init/ueventd.c

 


int ueventd_main(int argc, char **argv)

{

    struct pollfd ufd;

    int nr;

    char tmp[32];


    umask(000);



    signal(SIGCHLD, SIG_IGN);


    open_devnull_stdio();

    klog_init();

    

    INFO("starting ueventd\n");


    /* Respect hardware passed in through the kernel cmd line, Here we will look

    * for androidboot.hardware param in kernel cmdline, and save its value in

    * hardware[]. */

    import_kernel_cmdline(0, import_kernel_nv);

    get_hardware_name(hardware, &revision);


    ueventd_parse_config_file("/ueventd.rc");


    snprintf(tmp, sizeof(tmp),"/ueventd.%s.rc", hardware);

    ueventd_parse_config_file(tmp);


    device_init();


    ufd.events = POLLIN;

    ufd.fd = get_device_fd();


    while(1) {

        ufd.revents = 0;

        nr = poll(&ufd, 1, -1);

        if (nr <= 0)

             continue;

        if (ufd.revents == POLLIN)

               handle_device_fd();

    }

}




- 위는 ueventd 데몬의 메인 함수

- ueventd.rc 파일이나 ueventd.%hardware%.rc 파일의 내용을 읽어 해당 내용을 초기화

- device_init() 함수를 호출하여 sysfs 의 내용을 토대로한 /dev 디렉토리 업데이트를 처리하는 coldboot 수행



 

./system/core/init/devices.c



 void device_init(void)

{

    suseconds_t t0, t1;

    struct stat info;

    int fd;

#ifdev HAVE_SELINUX

    struct selinux_opt seopts[] = {

        { SELABEL_OPT_PATH, "/file_contexts" }

    };


    if(is_selinux_enabled() > 0)

       sehandle = selabel_open(SELABEL_CTX_FILE, seopts, 1);

#endif

    /* is 64K enough? udev uses 16MB! */

    device_fd = uevent_open_socket(64*1024, true);

    if(device_fd < 0)

        return;

    

    fcntl(device_fd, F_SETFD, FD_CLOEXEC);

    fcntl(device_fd, F_SETFD, O_NONBLOCK);


    if (stat(coldboot_done, &info) < 0) {

        t0 = get_usecs();

         coldboot("/sys/class");

         coldboot("/sys/block");

         coldboot("/sys/devices");

        t1 = get_usecs();

        fd = open(coldboot_done, O_WRONLY|O_CREAT, 0000);

        close(fd);

        //log_event_print("coldboot%ld uS\n", ((long)(t1 -t0)));

        ERROR("coldboot %ld uS\n", ((long)(t1 -t0)));

    } else {

        //log_event_print("skipping coldboot, already done\n")

        ERROR("skipping coldboot, already done\n");

    }

}



- 위는 device_init() 함수

- coldboot는 내부적으로 do_coldboot를 호출



 

./system/core/init/devices.c


static void do_coldboot(DIR *d)

{

    struct dirent*de;

    int dfd, fd;


    dfd = dirfd(d);


    fd = openat(dfd, "uevent", O_WRONLY);

    if(fd >=0) {

        write(fd, "add\n", 4);

        close(fd);

        handle_device_fd();

    }

    while((de = readdir(d))) {

        DIR *d2;


        if(de ->d_type != DT_DIR || de->d_name[0] =='.')

            continue;


        fd = openat(dfd, de->d_name, O_RDONLY | O_DIRECTORY);

        if(fd < 0)

            continue;


        f2 = fdopendir(fd);

        if(d2 == 0)

            close(fd);

        else {

            do_coldboot(d2);

            closedir(d2);

         }

    }


- 위는 do_coldboot() 의 내용

- ueventd 데몬은 이오ㅔ에도 커널 2.6에서 지원하는 hotplug 기능을 지원

- hotplug 기능은 커널의 uevent 발생을 실시간 모니터링하며 새로 업데이트된 sysfs 의 내용 (/sys)을 이용하여 디바이스 파일을 생성

- ueventd_main 함수에서 while 루프로 처리하는 부분이 커널에서 발생하는 uevent  를 모니터링 하는 부분이며 처리해야 이벤트 발생 시

   handle_device_fd() 호출




./system/core/init/devices.c

 

 

#define UEVENT_MSG_LEN 1024

void handle_device_fd()

{

    char msg[UEVENT_MSG_LEN+2];

    int n;

    while ((n = uevent_kernel_multicast_recv(device_fd, msg, UEVENT_MSG_LEN)) > 0) {

        if(n >= UEVENT_MSG_LEN) /* overflow -- discard */

            continue;


        msg[n] = '\0';

        msg[n+1] = '\0';


        struct uevent uevent;

        parse_event(msg, &uevent);

       

        handle_device_event(&uevent);

        handle_firmware_event(&uevent);

     }

}




- handle_device_event(&uevent), handle_firmware_event(&uevent) 함수 호출



./system/core/init/devices.c

 


static void handle_device_event(struct uevent *uevent)

{

    if (!strcmp(uevent->action,"add"))

        fixup_sys_perms(uevent->path);


    if (!strncmp(uevent->subsystem, "block",5)) {

        handle_block_device_event(uevent);

    } else if (!strncmp(uevent->subsystem, "platform", 8)) {

        handle_platform_device_event(uevent);

    } else {

        handle_generic_device_event(uevent);

    }

}

 



- handle_device_event() 함수는 각각 action 에 대해서 수행 (add, remove, link)

- add 인 경우 make_device(devpath, path, block, major, minor); 호출하여 mknod 수행



./system/core/init/devices.c 

 

static void handle_firmware_event(struct uevent *uevent)

{

    pid_t pid;

    int ret;

    

    if(strcmp(uevent->subsystem, "firmware"))

        return;


    if(strcmp(uevent->action, "add"))

        return;


    /* we fork, to avoid making large memory allocations in init proper */

    pid = fork();

    if (!pid) {

         process_firmware_event(uevent);

         exit(EXIT_SUCCESS);

    }

}



- handle_firmware_event() 함수는 firmware event 처리







출처 : http://hoonycream.tistory.com/entry/udev

'System Programming > Linux Kernel' 카테고리의 다른 글

라이브러리 로딩 ld.so.conf  (0) 2017.01.17
커널 타이머  (0) 2016.06.08
[Linux] ticket spin lock  (0) 2016.05.17
spin_lock, spin_lock_irq, spin_lock_irqsave  (2) 2016.05.17

Linux Kernel Timer

 

Linux 시간의 원천

  Linux 커널에서는 jiffies라는 글로벌 변수를 이용해서 측정하며, 이 변수는 시스템 시동 이후의 틱의 수를 식별합니다. 틱의 수를 계산하는 방법은 최저 레벨인 실행인 중인 특정 하드웨어 플랫폼에 따라 달라지지만 일반적으로 틱은 인터럽트를 통해 증가합니다. 틱 속도(jiffies의 최하위 비트)는 구성 가능하지만, 최근 x86용 2.6 커널의 경우에는 1틱이 4ms(250Hz) 입니다.

 

  jiffies 글로벌 변수는 커널 내에서 다양한 용도로 널리 사용되고 있으며, 대표적으로 타이머의 제한시간 값을 계산하기 위한 현재 절대 시간에 사용됩니다.

 

커널 타이머

  최근 2.6 커널의 타이머에는 몇 가지 다양한 스키마가 있습니다. 타이머 API가 대부분의 경우에 적합하기는 하지만 모든 타이머 중에서 가장 단순하고 정확성이 낮습니다. 이 API를 사용하면 jiffies 도메인(제한시간 최소 4ms )에서 작동하는 타이머를 생성 할수 있으며, 또한 시간이 나노 초 단위로 정의도는 타이머를 생성할 수 있는 고분해능 타이머(high-resolution timer) API도 있습니다. 

  프로세서 및 그 작동 속도에 따라 성능의 차이가 있을 수 있지만, 이 API를 사용하면 제한시간을 jiffies 틱 간격 이하로 스케줄할 수 있습니다.

 

표준타이머

 

  표준타이머 API는 Linux 커널의 초기 버전부터 오랫동안 Linux 커널에 포함되어 있었습니다. 고분해능 타이머(high-resolution timer)에 비해 정확도가 떨어지기는 하지만 실제 장치를 처리할 때 발생하는 오류 케이스를 처리하는 데 사용되는 일반적인 드라이버 제한시간에 이상적입니다. 이러한 제한시간은 실제로 초과되는 경우가 많지 않으며 대부분의 경우에는 시작 되었다가 제거되는 형태를 보인다.

 

  단순 커널 타이머는 타이머 휠을 사용하여 구현됩니다. 이 아이디어는 1997년 Finn Arne Gangstad 에 의해 처음 소개 되었습니다. 이 타이머는 많은 수의 타이머를 관리하는 데 문제점이 있지만, 이를 무시하고 전형적인 경우인 합리적인 수의 타이머를 관리할 경우 좋은 성능을 제공합니다. (원래 타이머 구현에서는 단순히 만기 순서에 양방향으로 연결된 상태로 타이머를 유지합니다. 개념적으로 단순하지만 이 방법은 확장 될 수 없습니다.) 타이머 휠은 버켓의 컬렉션이며, 각 버켓은 타이머 만기를 위한 미래의 시간을 나타낸다. 버켓은 5개의 버켓을 기반으로 하는 로그 시간(logarithmic time)을 사용하여 정의됩니다. jiffies를 시간 단위로 사용하여 미래의 만기 기간을 나타내는 많은 그룹을 정의 합니다. (각 그룹은 타이머 목록으로 표시됨) 타이머는 복잡도가 O(1) 이고 O(N) 시간 이내에 만기가 되는 목록 작업을 사용하여 삽입되며, 타이머 만기는 계단식으로 발생하게 됩니다.

 

  즉, 타이머의 만기 시간이 줄어들면 타이머가 정밀도가 높은 버켓에서 제거된 다음 정밀도가 낮은 버켓에 삽입됩니다.

 

타이머 API

 

  Linux에는 타이머의 생성과 관리를 위한 간단한 API가 있습니다. 이 API는 타이머 작성, 취소 및 관리를 위한 함수(및 헬퍼 함수)로 구성되어 있습니다.

 

  타이머는 timer_list 구조체로 정의되며, 이 구조체에는 타이머 구현에 필요한 모든 데이터(컴파일 시 구성되는 선택적 타이머 통계 및 목록 포인터 포함)가 담겨 있습니다. 사용자의 관점에서 볼 때, timer_list에는 만기 시간, 콜백함수(타이머의 만기 시간/여부) 및 사용자 제공 컨텍스트가 있습니다. 그런 다음 사용자는 타이머를 초기화 해주어야 합니다. 초기화는 몇가지 방법으로 수행되는 데, 가장 간단한 방법은 setup_timer를 호출하는 것입니다. 이 함수는 타이머를 초기화하고 사용자 제공 콜백 함수 및 컨텍스트를 설정합니다. 이 함수를 사용하지 않을 경우에는 사용자가 타이머에서 이러한 값(함수 및 데이터를)을 설정하고 init_timer를 호출 할 수 있습니다. init_timer는 setup_timer에서 내부적으로 호출됩니다.

 

 

void init_timer( struct timer_list *timer );
void setup_timer( struct timer_list *timer, 
                     void (*function)(unsigned long), unsigned long data );

 

 

 

  setup_timer로 타이머를 초기화한 다음에는 사용자가 mod_timer를 호출하여 만기 시간을 설정해야 합니다. 일반적으로 만기 시간은 미래이므로 여기에서는 jiffies를 추가하여 현재 시간을 기준으로 하는 오프셋을 설정합니다. 사용자는 del_timer를 호출하여 타이머를 삭제할 수 도 있습니다.

int mod_timer( struct timer_list *timer, unsigned long expires );
void del_timer( struct timer_list *timer );

 

 

 

  마지막으로 사용자는 timer_pending을 호출하여 타이머가 아직 만기되지 않고 보류 중인지 여부를 확인 할 수 있습니다. (타이머가 보류 중이면 1이 리턴됩니다.)

int timer_pending( const struct timer_list *timer );

 

 

타이머 예제

 

  이러한 API 함수 중 일부를 실제로 살펴봅시다. Listing 1 에서는 단순 타이머 API의 핵심 부분을 보여주는 단순 커널 모듈을 제공합니다. init_module에서 setup_timer를 사용하여 타이머를 초기화 한 다음 mod_timer를 호출하여 타이머를 시작합니다. 타이머가 만료되면 콜백 함수 (my_timer_callback)가 호출된다. 마지막으로 모듈을 제거하면 del_timer를 통해 타이머가 삭제됩니다.(del_timer의 리턴값을 검사하여 타이머가 사용 중인지 확인)

 

 

Listing 1.

 

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/timer.h>

MODULE_LICENSE( "GPL" );

static struct timer_list my_timer;

void my_timer_callback( unsigned long data )
{
  printk( "my_timer_callback called (%ld).\n", jiffies );
}

int init_module( void )
{
  int ret;

  printk("Timer module installing\n");

  // my_timer.function, my_timer.data
  setup_timer( &my_timer, my_timer_callback, 0 );

  printk( "Starting timer to fire in 200ms (%ld)\n", jiffies );
  ret = mod_timer( &my_timer, jiffies + msecs_to_jiffies(200) );
  if (ret) printk("Error in mod_timer\n");

  return 0;
}

void cleanup_module( void )
{
  int ret;

  ret = del_timer( &my_timer );
  if (ret) printk("The timer is still in use...\n");

  printk("Timer module uninstalling\n");

  return;
}

 

 

 

/include/linux/timer.h 에서 타이머 API에 대하 ㄴ자세한 정보를 볼 수 있습니다. 단순 타이머 API는 쉽고 효율적이지만 실시간 어플리케이션에 필요한 정확도를 제공하지 못합니다. 따라서 최근에 Linux에 추가되어 더 높은 분해능 타이머를 지원하는 기능이 있습니다.

 

 

고분해능 타이머(High-resolution timers)

 

 

  고분해능 타이머(hrtimer)는 앞에서 설명한 타이머 프레임워크와는 독립된 고정밀 타이머 관리 프레임워크를 제공합니다. 이는 두 프레임워크를 병합하기가 복잡하기 때문입니다. 타이머는 jiffies 단위로 작동하는 반면 hrtimer는 나노초 단위로 작동합니다.

 

  hrtimer 프레임워크는 일반적인 타이머 API와 다르게 구현됩니다. 버켓 및 타이머 캐스케이딩 대신 hrtimer는 타이머로 구성된 시간순 데이터 구조를 관리한다. (활성 시간에 처리 시간을 최소화하기 위해 타이머가 시간순으로 삽입됩니다.) 이 프레임워크에 사용되는 데이터 구조는 레드-블랙 트리입니다. 이 트리는 성능에 중점을 둔 어플리케이션에 이상적이며 일반적으로 커널 내의 라이브러리로 사용할 수 있습니다.

 

  hrtimer 프레임워크는 커널 내에서 API로 사용할 수 있으며 nanosleep, itimers 및 POSIX 타이머 인터페이스를 통해 사용자 공간 어플리케이션에서도 사용됩니다. 이 프레임워크는 2.6.21 커널에 포함되어 있습니다.

 

 

고분해능 타이머 API

 

  hrtimer API에는 기존 API와 유사한 점도 있지만 추가적인 시간 제어를 위한 몇 가지 근본적인 차이점도 있습니다. 첫번째 차이점은 시간이 jiffies가 아닌 ktime이라는 특수 데이터 유형으로 표시가 되는데, 이 표시는 이 단위에서 효율적인 시간 관리와 관련된 일부 세부 사항을 숨깁니다. 이 API는 절대 시간과 상대 시간을 구별하므로 호출자가 유형을 지정해야 합니다.

 

  기존 타이머 API와 마찬가지로 hrtimer도 구조체로 표현이 되는데, 이 구조체는 사용자 관점(콜백 함수, 만기 시간 등)에서 타이머를 정의하고 관리 정보를 통합합니다.( 이 경우에는 타이머가 레드-블랙 트리. 선택적 통계 등에 존재합니다.)

 

  먼저 hrtimer_init을 통해 타이머가 초기화 됩니다. 이 호출은 타이머, 클럭 정의 및 타이머 모드(한번 혹은 다시)를 포함합니다. 사용할 클럭은 /include/linux/time.h에 정의되어 있으며, 시스템에서 지원하는 다양한 클럭을 나타냅니다.(예 : 실시간 클럭 또는 시스템 시동과 같은 시작 시점의 시간을 나타내는 모노 클럭) 초기화된 타이머는 hrtimer_start를 사용하여 시작할 수 있습니다. 이 호출은 만기 시간(ktime_t) 및 시간 모드 값(절대값 또는 상대값)을 포함합니다.

void hrtimer_init( struct hrtimer *time, clockid_t which_clock, 
			enum hrtimer_mode mode );
int hrtimer_start(struct hrtimer *timer, ktime_t time, const 
			enum hrtimer_mode mode);

 

  hrtimer를 시작한 후에는 hrtimer_cancel 또는 hrtimer_try_to_cancel을 호출하여 최소 할 수 있습니다. 각 함수에는 중지할 타이머에 대한 hrtimer 참조가 들어 있으며, 두 함수의 차이점은 다음과 같다. hrtimer_cancel 함수는 타이머를 취소하려고 시도합니다. 하지만 타이머가 이미 시작되었으면 콜백함수가 종료될 때까지 기다립니다. 반면에, hrtimer_try_to_cancel 함수는 타이머를 취소하려고 시도할 때 타이머가 이미 시작되었으면 오류를 리턴합니다.

int hrtimer_cancel(struct hrtimer *timer);
int hrtimer_try_to_cancel(struct hrtimer *timer);

 

 

  hrtimer_callback_running을 호출하여 hrtimer가 해당 콜백을 활성화했는지 여부를 확인할 수 있습니다. 이 함수는 타이머의 콜백 함수가 호출되었을 때 오류를 리턴하기 위해 hrtimer_try_to_cancel에 의해 내부적으로 호출됩니다.

int hrtimer_callback_running(struct hrtimer *timer);

 

 

 

hrtimer 예제

 

  hrtimer API의 사용법은 Listing 2 에서 보여지듯이 매우 간단합니다. init_module 내에서 상대적인 제한시간(이 경우에는 200ms)을 정의하여 시작할 수 있습니다. hrtimer_init 호출을 사용하여 hrtimer를 초기화한(모노 클럭 사용) 다음에 콜백 함수를 설정합니다. 마지막으로 앞에서 작성한 ktime 값을 사용하여 타이머를 시작합니다. 타이머가 시작되면 my_hrtimer_callback 함수가 호출되면서 HRTIMER_NORESTART를 리턴합니다. 따라서 타이머가 자동으로 다시 시작되지 않게 됩니다. cleanup module 함수에서 hrtimer_cancel을 사용하여 타이머를 취소하면서 정리합니다.

 

Listing2. hrtimer API 탐색

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/hrtimer.h>
#include <linux/ktime.h>

MODULE_LICENSE( "GPL" );

#define MS_TO_NS(x)	(x * 1E6L)

static struct hrtimer hr_timer;

enum hrtimer_restart my_hrtimer_callback( struct hrtimer *timer )
{
  printk( "my_hrtimer_callback called (%ld).\n", jiffies );

  return HRTIMER_NORESTART;
}

int init_module( void )
{
  ktime_t ktime;
  unsigned long delay_in_ms = 200L;

  printk("HR Timer module installing\n");

  ktime = ktime_set( 0, MS_TO_NS(delay_in_ms) );

  hrtimer_init( &hr_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL );
  
  hr_timer.function = &my_hrtimer_callback;

  printk( "Starting timer to fire in %ldms (%ld)\n", delay_in_ms, jiffies );

  hrtimer_start( &hr_timer, ktime, HRTIMER_MODE_REL );

  return 0;
}

void cleanup_module( void )
{
  int ret;

  ret = hrtimer_cancel( &hr_timer );
  if (ret) printk("The timer was still in use...\n");

  printk("HR Timer module uninstalling\n");

  return;
}

 

 

 

hrtimer API에는 지금까지 살펴본 것보다 훨씬 더 많은 특성이 있습니다. 한가지 흥미로운 특성은 콜백 함수의 실행 컨텍스트(예 : softirq, hardirq 컨텍스트)를 정의할 수 잇따는 것입니다. /include/linux/hrtimer.h의 포함 파일에서 hrtimer API에 대한 자세한 정보를 볼 수 있습니다.

 

 

 

커널 목록

 

  앞에서 언급한 것처럼 목록은 아주 유용한 구조입니다. 따라서 커널에서는 일반적인 용도로 사용할 수 잇는 효율적인 구현을 제공합니다. 또한 지금까지 살펴본 API에도 목록이 있습니다. 양방향 연결 목록 API를 이해하면 이 효율적인 데이터 구조를 사용하여 개발 작업을 수행하고 목록을 활용하는 커널의 코드를 이해하는 데 많은 도움이 됩니다. 이제 커널 목록 API를 살펴보도록 합시다.

 

  이 API는 목록 헤드(앵커)뿐만 아니라 구조체 내 목록 포인터를 나타내는 데도 사용되는 list_head 구조체를 제공합니다. 목록 기능이 포함된 샘플 구조체는 Listing 3에 나와 있습니다. 이 샘플 구조체에는 오브젝트 연결에 사용되는 list_head 구조체가 추가 되어 있습니다. 그리고 일부 GCC magic (/include/kernel/kernel.h 에 정의된 list entry 및 container_of)을 통해 구조체의 어디에서나 list_head 구조체를 추가할 수 있으며 목록 포인터에서 수퍼 오브젝트로 역참조 할 수 있습니다.

 

Listing 3. 목록 참조를 사용하는 샘플 구조체

	
struct my_data_structure {
	int value;
	struct list_head list;
};

 

 

 

  모든 목록 구현에서와 마찬가지로 목록에 대한 앵커 역할을 수행할 목록 헤드가 필요합니다. 이 작업은 일바넞긍로 목록에 대한 선언 및 초기화를 제공하는 LIST_HEAD 매크로를 통해 수행됩니다. 이 매크로는 사용자가 오브젝트를 추가할 수 잇는 list_head 오브젝트 구조체를 작성합니다.

LIST_HEAD( new_list )

 

 

 

  또한 LIST_HEAD_INIT 매크로를 사용하여 수동으로 목록 헤드를 작성할 수 있습니다.( 예를들어, 목록 헤드가 다른 구조체에 있는 경우)

기본 초기화가 완료되면 list_add 및 list_del 함수를 포함한 여러 함수를 사용하여 목록을 조작할 수 있습니다. 이제 이 API의 사용법을 더 잘 보여주는 예제 코드를 살펴 보도록 합시다.

 

목록 API 예제

 

  Listing 4에서는 수많은 목록 API 함수를 탐색하는 간단한 커널 모듈을 제공합니다.(/include/linux/list.h 에서 많은 함수를 찾아볼 수 있습니다.) 이 예제에서는 두개의 목록을 작성하고, init_module 함수를 이용하여 목록을 채운 다음 cleanup_module 함수에서 조작합니다.

 

  먼저 일부 데이터와 두개의 목록 헤더를 포함하는 데이터 구조(my_data_struct)를 작성합니다. 이 예제에서는 한 오브젝트를 여러 목록에 동시에 삽입할 수 있다는 것을 보여줍니다. 그런 다음 두개의 목록 헤드(my_full_list 및 my_odd_list)를 작성합니다.

 

  init_module 함수에서 10개의 데이터 오브젝트를 작성한 다음 list_add 함수를 사용하여 목록에 로드합니다.(my_full_list에 모든 오브젝트를 로드하고 my_odd_list에 모든 홀수 값 오브젝트를 로드합니다.) list_add는 두개의 인수를 사용하는데, 첫 번째 인수는 사용할 오브젝트 내의 목록 참조이고 두번째 인수는 목록 앵커입니다. 이는 목록 참조가 있는 수퍼 오브젝트를 식별하는 커널의 내부 기능을 사용하여 데이터 오브젝트를 여러 목록에 로드할 수 있음을 보여 줍니다.

 

  cleanup_modlue 함수는 목록 API의 몇 가지 추가 기능을 보여 줍니다. 첫번째 기능은 목록 반복을 쉽게 수행할 수 있도록 지원하는 list_for_each 매크로 입니다. 이 매크로를 사용하려면 현재 오브젝트에 대한 참조(pos)와 반복할 목록 ㅊ마조를 제공해야 합니다. 각 반복 마다 list_head 참조가 리턴되며, 리턴된 참조를 list_entry에 제공하여 컨테이너 오브젝트(사용자의 데이터 구조)를 식별할 수 있습니다. 사용자의 구조와 구조 내의 목록 변수를 지정합니다. 이 변수는 내부적으로 컨테이너를 역참조하는 데 사용됩니다.

 

  홀수 목록을 표시하기 위해 또 하나의 반복 매크로인 list_for_each_entry를 사용합니다 . 이 매크로는 사용자의 데이터 구조를 자동으로 제고하므로 list_entry를 수행하지 않아도 되기 때문에 더 간단합니다.

 

  마지막으로 할당된 요소를 해제하기 위해 list_for_each_safe를 사용하여 목록을 반복합니다. 이 매크로를 사용 하면 목록 항목을 제거(반복 작업의 일부로서 수행할 작업)하지 않으면서 안전하게 목록을 반복할 수 있습니다. list_entry를 사용하여 데이터 오브젝트를 찾은(커널 풀에 다시 해제하기 위해) 다음 list_del을 사용하여 목록의 항목을 해제합니다.

 

 

Listing 4. 목록 API 탐색

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/list.h>

MODULE_LICENSE( "GPL" );

struct my_data_struct {
  int value;
  struct list_head full_list;
  struct list_head odd_list;
};

LIST_HEAD( my_full_list );
LIST_HEAD( my_odd_list );


int init_module( void )
{
  int count;
  struct my_data_struct *obj;

  for (count = 1 ; count < 11 ; count++) {

    obj = (struct my_data_struct *)
            kmalloc( sizeof(struct my_data_struct), GFP_KERNEL );

    obj->value = count;

    list_add( &obj->full_list, &my_full_list );

    if (obj->value & 0x1) {
      list_add( &obj->odd_list, &my_odd_list );
    }

  }

  return 0;
}


void cleanup_module( void )
{
  struct list_head *pos, *q;
  struct my_data_struct *my_obj;

  printk("Emit full list\n");
  list_for_each( pos, &my_full_list ) {
    my_obj = list_entry( pos, struct my_data_struct, full_list );
    printk( "%d\n", my_obj->value );
  }

  printk("Emit odd list\n");
  list_for_each_entry( my_obj, &my_odd_list, odd_list ) {
    printk( "%d\n", my_obj->value );
  }

  printk("Cleaning up\n");
  list_for_each_safe( pos, q, &my_full_list ) {
    struct my_data_struct *tmp;
    tmp = list_entry( pos, struct my_data_struct, full_list );
    list_del( pos );
    kfree( tmp );
  }

  return;
}

 

 

헤드 대신 목록의 후미에 추가하고 (list_add_tail), 목록을 결합하고(list_splice), 목록의 컨텐츠를 테스트하는(list_empty) 등의 기능을 제공하는 여러 다양한 함수가 있으니, 더 많은 자세한 정보를 찾아 보시기 바랍니다.

 

 

 

끝으로

 

  필요에 따라 기능을 분리하는 기능(타이머 및 고정밀 타이머) 뿐만 아니라 코드 재사용을 위해 코드를 일반화하는 기능(목록 API)을 보여주는 몇 가지 API를 설명하였습니다.

 

  일반 타이머는 일반 드라이버 제한시간에 효율적인 메커니즘을 제공하는 반면 hrtimer는 정밀도 높은 타이머 기능을 위한 높은 서비스 품질을 제공합니다.

 

  목록 API는 매우 일반적이지만 효율적이고 다양한 기능을 갖춘 인터페이스를 제공합니다.

 

커널 코드를 작성하게 되면 이러한 세 API 중 하나 이상을 실행 할 것이므로 깊은 이해가 필요한 주제 였습니다.

 

 

 

'System Programming > Linux Kernel' 카테고리의 다른 글

라이브러리 로딩 ld.so.conf  (0) 2017.01.17
udev  (0) 2016.09.12
[Linux] ticket spin lock  (0) 2016.05.17
spin_lock, spin_lock_irq, spin_lock_irqsave  (2) 2016.05.17

Linux: 2.6.30
arch: x86


spin lock은 mutiprocessor system에서
여러 processor가 동시에 critical section에 진입하지 못하도록 하는 synchronization 기법이다.
한 processor가 lock을 가지고 있으면 다른 processor들은 unlock될 때까지 busy-wait하다가
lock을 차지하기 위해 동시에 lock 변수에 접근(write)한다.

여기서 두 가지 문제가 발생할 수 있는데
첫 번째는 각 processor 간에 lock을 획득하는 순서를 보장할 수 없기 때문에
먼저 spin lock을 기다리던 processor가 더 나중에 lock을 얻을 수도 있다는 것이다.
때문에 spin lock은 공정하지 못하다.

또 하나의 문제는 성능에 관련된 것으로
cache coherency로 인해 한 processor가 lock 변수에 write를 하게되면
다른 모든 processor의 cache line이 invalidate된다.
따라서 contention이 심한 경우 lock을 얻은 processor에서도 반복적으로 cache miss가 발생하여
실행 성능이 매우 나빠질 수 있다. (보통 lock 변수와 데이터는 같은 line에 놓여있을 것이다.)

ticket spin lock은 이를 개선하기 위해 2.6.25 버전부터 도입된 것으로
lock을 기다리는 각 processor들은 자신 만의 ticket을 부여받고
자기 차례가 돌아오는 경우에만 write를 시도하므로
순서대로 lock을 얻을 수 있으며 전체적으로 cache miss 횟수를 줄일 수 있다.

그럼 코드를 살펴보자.
spin_lock()은 다음과 같이 정의되어 있다.

/* include/linux/spinlock.h */ 
#define spin_lock(lock) _spin_lock(lock)
 /* kernel/spinlock.c */ 
void __lockfunc _spin_lock(spinlock_t *lock) 
{ 
           preempt_disable(); 
           spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); 
           LOCK_CONTENDED(lock, _raw_spin_trylock, _raw_spin_lock); 
} 

먼저 커널 선점 기능을 disable한 후에 spin_acquire()를 호출하는데이는 CONFIG_LOCKDEP가 선택된 경우 lock을 얻으려는 processor의 정보를 기록하기 위한 것이다.

LOCK_CONTENDED의 경우도 비슷한데 CONFIG_LOCKSTAT이 설정되지 않은 경우에는
단순히 _raw_spin_lock()을 호출하는 코드로 확장된다.
_raw_spin_lock도 CONFIG_DEBUG_SPINLOCK이 설정되지 않았다면
단순히 __raw_spin_lock()을 호출하는 코드로 확장된다.
__raw_spin_lock 계열의 함수는 architecture-specific 함수로 x86의 경우 다음과 같이 정의된다.

static __always_inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    __ticket_spin_lock(lock);
}

static __always_inline int __raw_spin_trylock(raw_spinlock_t *lock)
{
    return __ticket_spin_trylock(lock);
}

static __always_inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
    __ticket_spin_unlock(lock);
}

위에서 볼 수 있듯이 단순히 ticket_spin_lock 계열의 함수를 호출하는 방식으로 구현되어 있다.

먼저 간단한 unlock의 경우부터 살펴보기로 하자.
ticket spin lock은 processor의 수가 256 개를 넘어가는 머신의 경우를 구분하여 구현되어 있는데
여기서는 이 부분은 무시하고 NR_CPUS 값이 256 이하인 경우 만을 살펴볼 것이다.
따라서 spin lock을 기다라는 모든 processor의 정보는 한 byte 내에 포함할 수 있다. (TICKET_SHIFT = 8)

static __always_inline void __ticket_spin_unlock(raw_spinlock_t *lock)
{
    asm volatile(UNLOCK_LOCK_PREFIX "incb %0"
             : "+m" (lock->slock)
             :
             : "memory", "cc");
}


 

unlock은 단순히 raw_spinlock_t 구조체의 slock 변수를 1 증가시키는 일만 수행한다. 여기서 주의깊게 살펴보아야 할 부분은 incb로 최하위 바이트의 값 만을 증가시킨다는 것이다.

lock 변수는 개념적으로 두 부분으로 나누어지는데
위에서 언급한대로 NR_CPUS가 256보다 작은 경우에는 하위 두 바이트를 사용하며
상위 바이트는 lock을 기다리는 processor들을 위한 ticket 값이고 (next)
하위 바이트는 현재 lock을 가지고 있는 processor의 ticket 값이다. (owner)

nlock()이 호출되면 현재 processor가 lock을 반환한다는 의미이므로
다음 processor가 lock을 얻을 수 있도록 owner ticket을 증가시킨다.
lock은 자기가 보관하고 있는 next ticket 값과 owner ticket 값이 일치하는 경우에 얻는다.

 
static __always_inline void __ticket_spin_lock(raw_spinlock_t *lock) 
{ 
          short inc = 0x0100;
          asm volatile ( LOCK_PREFIX "xaddw %w0, %1\n"
                          "1:\t" "cmpb %h0, %b0\n\t" 
                          "je 2f\n\t" "rep ; nop\n\t" 
                          "movb %1, %b0\n\t"
                           /* don't need lfence here, because loads are in-order */
                          "jmp 1b\n" "2:" : "+Q" (inc), 
                          "+m" (lock->slock) : : "memory", "cc"); 
} 

 

이 코드는 (동기화와 관련된 문제를 제외하면) 개념적으로 아래와 같다.

static __always_inline void __ticket_spin_lock(raw_spinlock_t *lock)
{
    short inc = 0x100;
    short tmp = (short) lock->slock;
    lock->slock += inc;

    while ((tmp >> 8) != (tmp & 0xFF)) {
        cpu_relax();
        tmp = (tmp & 0xFF00) | (unsigned char) lock->slock;
    }
}

 

우선 lock->slock 값을 읽어오고 동시에 next ticket을 증가시킨다. (lock; xaddw)읽어온 slock의 상위 바이트는 현재 processor의 ticket 값으로 저장하고

동시에 다음 processor가 lock을 얻기 위한 ticket을 증가시키는 것이다.
이는 LOCK_PREFIX가 붙어있으므로 atomic하게 수행된다.

그리고는 증가시키기 전의 slock 변수에서 next와 owner ticket이 동일한지 검사한다. (cmpb %h0, %b0)
만약 같다면 현재 processor가 lock을 얻은 것이므로 loop을 종료하고 critical section에 진입한다.
그렇지 않다면 계속 slock의 owner ticket 값을 갱신한 후 다시 검사한다. (movb %1, %b0)

즉, unlock이 수행된 후에 lock을 얻기 위해 다시 lock 변수를 write하지 않아도 된다!

trylock()은 slock 값을 먼저 살펴본 후 lock을 얻을 수 있는 경우에만 slock 변수를 write한다.

 

static __always_inline int __ticket_spin_trylock(raw_spinlock_t *lock)
{
    int tmp, new;

    asm volatile("movzwl %2, %0\n\t"
             "cmpb %h0,%b0\n\t"
             "leal 0x100(%k0), %1\n\t"
             "jne 1f\n\t"
             LOCK_PREFIX "cmpxchgw %w1,%2\n\t"
             "1:"
             "sete %b1\n\t"
             "movzbl %b1,%0\n\t"
             : "=&a" (tmp), "=&q" (new), "+m" (lock->slock)
             :
             : "memory", "cc");

    return tmp;
}

 

이 코드는 (동기화와 관련된 문제를 제외하면) 개념적으로 아래와 같다.

static __always_inline void __ticket_spin_lock(raw_spinlock_t *lock)
{
    short inc = 0x100;
    short tmp = (short) lock->slock;
    lock->slock += inc;

    while ((tmp >> 8) != (tmp & 0xFF)) {
        cpu_relax();
        tmp = (tmp & 0xFF00) | (unsigned char) lock->slock;
}


우선 lock->slock 값을 읽어오고 동시에 next ticket을 증가시킨다. (lock; xaddw)

읽어온 slock의 상위 바이트는 현재 processor의 ticket 값으로 저장하고
동시에 다음 processor가 lock을 얻기 위한 ticket을 증가시키는 것이다.
이는 LOCK_PREFIX가 붙어있으므로 atomic하게 수행된다.

 

그리고는 증가시키기 전의 slock 변수에서 next와 owner ticket이 동일한지 검사한다. (cmpb %h0, %b0)
만약 같다면 현재 processor가 lock을 얻은 것이므로 loop을 종료하고 critical section에 진입한다.
그렇지 않다면 계속 slock의 owner ticket 값을 갱신한 후 다시 검사한다. (movb %1, %b0)
즉, unlock이 수행된 후에 lock을 얻기 위해 다시 lock 변수를 write하지 않아도 된다!

trylock()은 slock 값을 먼저 살펴본 후 lock을 얻을 수 있는 경우에만 slock 변수를 write한다.


이 코드는 개념적으로 아래와 같다.

static __always_inline int __ticket_spin_trylock(raw_spinlock_t *lock)
{
    int tmp, new;

   tmp = (short) lock->slock;
    if ((tmp >> 8) != (tmp & 0xFF)) {
        return 0;

    new = tmp + 0x100;
    if (tmp == (short) lock->slock) {
        lock->slock = new;
        tmp = 1;
    } else {
        tmp = 0;
    }
    return tmp;
}

 

먼저 현재 slock 변수의 값을 읽어서 lock을 기다리는 processor가 있는지 확인한다. (movzwl %2, %0)
이는 next ticket과 owner ticket이 다른 경우이므로 바로 0을 return한다. (cmpb %h0, %b0)
그렇지 않다면 next ticket을 1 증가시켜두는데 (leal 0x100(%k0), %1)
이는 tmp(%0)의 값을 new(%1)로 옮기고 new 값을 증가시키는 작업을 한 번에 처리해주는 hack이다.

그 동안 slock값이 바뀌지 않았다면 증가시킨 값으로 갱신한다. (lock; cmpxchgw %w1,%2)
이도 LOCK_PREFIX가 붙어있으므로 atomic하게 수행된다.
cmpxchg의 결과는 ZF 플래그에 저장되므로 이 값을 읽어서 new 변수의 최하위 바이트에 저장하고 (sete %b1)
이를 다시 tmp 변수에 옮긴 후 return한다. (movzbl %b1,%0)

'System Programming > Linux Kernel' 카테고리의 다른 글

라이브러리 로딩 ld.so.conf  (0) 2017.01.17
udev  (0) 2016.09.12
커널 타이머  (0) 2016.06.08
spin_lock, spin_lock_irq, spin_lock_irqsave  (2) 2016.05.17

+ Recent posts