ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 디바이스 드라이버 / 개발 시 고려사항
    Linux/Linux Device Driver 2020. 9. 1. 12:38

    변수

    지역 변수와 전역 변수의 선택

    다비아스 드라이버는 다중 프로세스 환경에서 동작하기 때문에 프로세스간 변수 사용에 따른 경쟁 문제(동기화 문제)를 해결하기 위해 가급적 지역 변수를 사용하는 편이 좋다. 그러나 디바이스 드라이버가 커널에 적재되고 해제되는 시점까지 유지해야하는 정보라면 전역 변수로 지정해야 한다.

    중복 함수명과 변수명 방지

    리눅스 커널은 많은 함수와 전역 변수가 있다. 그래서 새로 추가되는 디바이스 드라이버 소스에서 사용되는 함수와 변수가 중복될 수 있다. 모듈로 작성되는 경우라면 심볼 테이블에 의해 관리되기 때문에 중복으로 인해 문제가 발생할 확률이 적어지지만, 커널 소스에 포함시키는 경우라면 심각한 문제가 발생할 수 도 있다. 그러므로 이를 미연에 방지하기 위해 함수나 변수에 static이라는 키워드를 사용하는 습관을 들이는 것이 좋다.

     

    static 키워드를 함수나 변수에 선언에서 사용하면 해당 파일 소스에서만 참조한다. 그러므로 링크 과정에서는 해당 함수명이나 변수명을 심볼 테이블에 유지하지 않는다. 그래서 다른 디바이스 드라이버가 외부 참조를 선언하는 경우에는 static을 사용하면 안 된다. 또한 지역 변수에 static 선언을 하는 것은 의미가 약간 다르므로 지역 변수에서는 사용하면 안 된다. 지역 변수에서 static을 사용하면 전역 변수와 같이 함수가 종료되더라도 값이 유지된다. 전역 변수와 다른 점은 외부 심볼 참조가 되지 않는 점이 다를 뿐이다.

     

    변수의 데이터형

    변수의 데이텨형은 프로세스에 의존적이다.

    16비트 프로세스에서 int 형 변수의 크기는 16비트

    32비트 프로세스에서 int 형 변수의 크기는 32비트

    64비트 프로세스에서 int 형 변수의 크기는 64비트이다.

    이런 변수 타입의 크기 차이는 다른 플랫폼간의 이식성에 나쁜 영향을 준다. 이런 문제를 해결하기 위해서는 가급적 리눅스 커널에서 제공되는 데이터형을 사용하는 것이 좋다.

     

    /*

    데이터 형은 #include <asm-generic/types.h>에 선언되어 있다. /usr/include/asm-generic/types.h

    */

     

    구조체

    C언어의 구조체는 컴파일러마다 다를 수 있지만, 보통 int 형 크기의 배수로 선언된다. 대부분의 경우 이런 특성이 문제가 되지 않지만 컨트롤러의 레지스터를 표현하거나 응용프로그램과의 정확한 데이터 교환을 위해 int 형의 배수로 정렬되면 곤란한 경우가 종종 발생하므로, 이런 경우에는 packed라는 키워드를 사용해 실제 변수의 크기로 선언하는 것이 좋다.

     

    typedef struct{

       u16 index;

       u16 data;

       u8 data2

    } __attribute__ ((packed)) testctl_t;

     

    바이트 순서

    다른 플랫폼에 이식할 경우에는 정수 바이트 순서에 주의해야 한다. 프로세서마다 정수 데이터를 메모리에 표현할 때 가장 작은 바이트를 두는 위치가 다르기 때문이다. 낮은 바이트를 먼저 저장하면 리틀 엔디언이라고 하고 반대로 낮은 바이트를 마지막에 저장하면 빅 엔디언이라고 한다. 프로세스에 따라 바이트 순서를 정확하게 처리하도록 도와주는 헤더 파일은 /usr/include/linux/byteorder 디렉터리에 있다.

     

    데이터 정렬

    데이터의 효율적인 처리를 요구하거나 데이터가 정렬되어 있지 않으면 예외 처리가 수행되는 프로세서가 있다. 데이터가 정렬된다는 것은 32비트 프로세스의 경우 4바이트 단위로 정렬되는 것을 의미한다. 보통 컴파일러의 링크 스크립트에서 정렬 처리를 지정하지만, 동적인 데이터를 다룰 경우에는 커널 내의 매크로 함수의 도움을 받을 수 있다.

     

    #include <asm-gerneric/unaligned.h>

    get_unaligned(ptr)       // ptr 주소에서 값을 읽는다.

    put_unaligned(val, ptr)  // ptr 주소에 val 값을 쓴다.

     

    실제로 이 두 매크로를 사용하는 디바이스 드라이버는 주로 네크워크 디바이스나 IDE 디바이스에서 볼 수 있다.

     

    I/O 메모리 접근 변수 처리

    프로세서 중에서는 I/O를 처리하는 명령이 따로 있는 경우(입출력 맵 입출력)와 그렇지 않은 경우(메모리 맵 입출력)가 있다. I/O 처리 명령이 별도로 있으면 해당 명령을 사용하면 되지만, 메모리 번지를 이용하는 I/O 접근 처리의 경우에는 처리에 주의를 기울여야 한다.

     

    최적화 컴파일 옵션에 따라 의도하지 않은 결과가 발생할 수 있다. 이때 volatile을 이용하면 반드시 메모리에 접근하여 처리된다. 레지스터로 대체하는 것과 같은 최적화를 하지 않는다.

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    초기화와 종료

     

    디바이스 드라이버는 다음과 같이 두 가지 초기화 처리 시점과 종료 처리 시점이 존재한다.

     

    모듈 적재 시점과 모듈 제거 시점

    - insmod 명령 -> xxx_init -모듈 적재 과정

    - rmmod 명령 -> xxx_exit -모듈 제거 과정

     

    응용 프로그램이 디바이스 파일을 여는 시점과 닫는 시점

    - open() -> xxx_open() - 디바이스 파일을 여는 과정

    - close() -> xxx_close() - 디바이스 파일을 닫는 과정

     

    이 두 가지의 차이를 간단하게 설명하면 다음과 같다.

    하드웨어를 사용하기전에 미리 초기화를 해놓느냐 아니면 하드웨어를 사용할 때 초기화를 하느냐다.

     

    이 중 어디에서 초기화를 해야 한다는 표준적인 규정은 없다. 하드웨어의 구조와 디바이스 드라이버의 구현 방식에 따라 달라지기 때문이다. 하지만 각 처리 시점의 특성과 처리해야 하는 항목에 공동점이 있어 대략적인 기준은 있다.

     

    모듈 적재 과정

    1. 디바이스 드라이버 등록

    문자 디바이스 드라이버라면 register_chrdev() 함수등으로 디바이스 드라이버를 등록해야한다. 당연하다. open() 함수로 디바이스 파일을 열어 제어하려면 디바이스 드라이버가 등록되어 있어야 한다. 그래서 모듈이 적재할 때 실행되는 xxx_init() 함수에서 무조건 디바이스 드라이버를 등록해야한다.

     

    2. 하드웨어 검출 및 에러 처리

    커널 자원을 효울적으로 관리하기 위해서는 필요 없는 디바이스 드라이버는 가급적 커널에 포함시키지 않아야 한다. 그래서 디바이스 드라이버가 관리하는 하드웨어가 존재하는지 확인해 하드웨어가 검출되지 않으면 모듈이 커널에 적재되는 시점에 거부하도록 하는것이 좋다.

     

    즉 하드웨어가 고장이나 오류로 검출되지 안되면 해당 하드웨어를 제어하는 디바이스 드라이버는 필요가 없다. 그래서메모리를 차지하지 않게 적재시키지 않는것이다.

     

    모듈 제거 과정

    1. 디바이스 드라이버 제거

    디바이스 드라이버는 커널 자원을 점유하기 때문에 사용하지 않으면 제거해야한다.

     

    디바이스 파일을 여는 과정

    1. 모듈 사용 횟수 증가

     

     

     

    디바이스 파일 닫는 과정

    1. 모듈 사용 횟수 감소

     

     

    디바이스 드라이버 성격에 따라 초기화 한다.

    1. 디바이스 드라이버에 내부 구조체의 메모리 할당

    디바이스 드라이버에 내부에서의 메모리 할당은 다음과 같이 2가지로 나뉠 수 있다.

     

    1. 인터럽트 컨텍스트 환경에서의 메모리 할당

    보통 코드를 작성할때 디바이스 드라이버에서 인터럽트 서비스 함수를 만들고 등록하는 경우가 있다. 이 때 디바이스 드라이버의 인터럽트 서비스 함수내에서 메모리 할당을 하는것이 인터럽트 컨텍스트 환경에서의 메모리 할당이다.

     

     

     

    2. 프로세스 컨텍스트 환경에서의 메모리 할당

    디바이스 드라이버의 인터럽트 서비스 함수가 아닌곳에서의 메모리 할당이 프로세스 컨텍스트 환경에서의 메모리 할당이다. 프로세스가 동적하는 상황에서 kmalloc() 함수로 메모리를 할당할 때 GFP_KERNEL 인자값을 사용하면 메모리가 충분치 않을 경우에 디바이스 드라이버를 호출한 프로세스가 수행을 멈추고, 동적 메모리를 할당할 수 있는 상태가 될 때까지 잠든다. 그래서 모듈 적재 과정에서 미리 메모리 할당을 해놓으면 하드웨어를 사용할 때 프로세스가 잠들어 지연되는 상황을 예방할 수있다. 하지만 미리 메모리를 할당하기 때문에 하드웨어를 당장 사용하지 않는다면 그렇게 좋지 못한 방법이다.

     

    2 여러 프로세스가 하나의 디바이스에 접근하는 경우에 필요한 사전 처리

    디바이스 드라이버는 여러 프로세스에서 동시에 접근 할 수 있다. 그러므로 경쟁 처리와 관련된 변수나 구조체를 초기화해야 한다.

     

    (뮤텍스 스핀락)

     

    3. 주번호에 종속된 부 번호를 관리하기 위한 사전 처리

    일반적으로 주 번호 하나에 할당된 디바이스 드라이버는 하드웨어 하나를 처리한다. 그러나 하드디스크나 직렬 디바이스를 다루는 디바이스 드라이버처럼 부번호에 따라 다른 하드웨어를 관리하는 경우도 있다. 혹은 디바이스 드라이버 하나에서 부 번호에 따라 동작하는 방식이 달라지게 구현 할 수도 있다. 이런 처리는 디바이스 파일을 열었을 때 호출되는 open() 함수를 처리할 때 대부분 구현된다.

    4. 하드웨어 초기화

    하드웨어 초기화는 모듈 초기화 함수를 호출하는 시점에 구현할 수도 있고, 열기 처리 시점에 구현할 수도 있다. 언제 하드웨어 초기화를 하는 것이 좋은가는 작성되는 디바이스 드라이버의 성격에 따라 달라진다.

    열기 시점에 하드웨어를 초기화를 처리해야 하는 경우에는 가장 먼저 해당 하드웨어 장치의 다른 응용 프로그램에서 사용하고 있는가를 확인하는 절차가 필요하고, 사용중이라면 사용을 거부할 것인지 아니면 동시에 사용 가능하도록 처리해야 할 것인지를 개발자가 결정해야 한다.

     

     

     

     

     

     

     

     

     

     

     

     

     

    read write

     

    유저영역이 커널영역에서 데이터를 받기 위해서는

     

    문자배열은 초기화로 ""은되지만 초기화가 아닌상태에서는 ""가 안됨

     

    xxx_write() 함수에서는 const char * 매개변수가 가리키는 값을 그대로 읽을 수는 있지만 가리키는 값을 커널 영역으로 복사하지는 못한다. 그래서 copy_from_user() 함수를 사용하여 커널영역으로 복사해야한다.

     

    xxx_write() 함수에서는 문자열 포인터를 사용할 경우 kmalloc() 함수로 동적 메모리 할당을 해야한다. 

     

    c

     

     

     

     

     

    인터럽트 처리

    인터럽트 서비스 함수 내의 메모리 할당

    인터럽트 서비스 함수는 발생되는 인터럽트마다 특성이 서로 다르다. 그래서 인터럽트 서비스 함수의 처리 루틴을 작성할 때는 메모리 할당에 주의해야한다. 주로 사용되는 메모리 할당으로는 kmalloc() 함수와 kfree() 함수를 사용한다. vmalloc() 함수나 vfree() 그리고 ioremap() 같은 함수를 인터럽트에서 사용하면 안된다. kmalloc() 함수 역시 GFP_ATOMIC 인자를 사용한 방식만 사용해야 한다는 제약이 있다. 그러나 vmalloc() 함수나 kmalloc() 함수를 사용해서 인터럽트가 발생하기 전에 미리 할당한 메모리는 아무런 제약없이 사용할 수 있다.

     

    인터럽트 함수와 디바이스 드라이버간의 데이터 공유

    인터럽트 서비스 함수는 프로세스 문맥과 별개로 동작한다. 즉 인터럽트 서비스 함수가 수행할 때 어떤 디바이스 드라이버가 연관이 되어 있는지를 인터럽트 서비스 함수 내에서는 알 수가 없다. 인터럽트라는 특성 자체가 임의의 시점에서 수행되기 때문에 비록 인터럽트 서비스 함수와 관련있는 디바이스 드라이버가 동작하지 않는다 할지라도 인터럽트는 처리된다. 따라서 인터럽트 서비스 함수와 디바이스 드라이버 함수간에는 데이터를 공유하는 방법이 필요하다. 디바이스 드라이버와 연관된 정보를 공유하는 방법에는 두 가지가 있다. 하나는 전역 변수를 이용하는 방법이고, 다른 하나는 dev_id 매개 변수를 사용하는 방법이다.

     

    전역 변수를 이용하는 방법

    이 방식은 인터럽트 하나에 인터럽트 서비스 함수 하나가 동작하는 경우에 사용하지만, 동일한 인터럽트 서비스 함수로 여러 디바이스를 제어하는 경우에는 사용할 수 없다.

     

    dev를 이용하여 데이터를 공유하는 경우

    여러 디바이스를 하나의 인터럽트 서비스 함수에서 사용할 수 있도록 전역 변수를 사용하지 않고, 다음과 같은 형식을 주로 사용한다. 이런 방식은 디바이스의 다른 정보를 전달하는 가장 보편적인 방법으로 다중 프로세스 환경에 매우 적합하다.

     

    이 경우는 open() 함수에서 공유하고 싶은 변수(구조체)를 동적할당하여 request_irq() 함수의 dev매개변수와 file 포인터 변수가 가리키는 private_data변수에 넣어주므로써 인터럽트 함수와 디바이스 드라이버간의 데이터를 공유할 수 있다.

     

    인터럽트 서비스 등록과 해제 시점

    대부분의 디바이스 드라이버는 응용프로그램이 디바이스 파일을 열었을 때 인터럽트 함수를 등록하고, 디바이스 파일을 닫았을 때 인터럽트 함수를 제거한다. 왜 이렇게 open() 함수와 close() 함수에서 인터럽트 서비스 함수 등록과 해제 루틴을 넣을까?

    인터럽트의 수에 비해 만들어지는 제품이 수도 없이 많기 때문이다. 이런 문제 때문에 리눅스 커널은 이를 효율적으로 관리할 필요가 생겼고, 이를 위해 인터럽트를 공유할 수 있는 방법을 제공하게 되었다. 이 기능을 사용하려면 quest_irq() 함수의 매개변수 중 flags에 SA_SHIRQ를 설정하면 된다. 이렇게 하면 같은 인터럽트 번호에 여러 디바이스 드라이버 서비스 함수를 등록할 수 있다. 이 기능을 사용할 때 주의할 점은 여러 디바이스 드라이버가 동일한 인터럽트 번호를 사용하게 되므로 특정 디바이스 드라이버에 등록된 인터럽트 서비스 루틴이 다른 디바이스의 인터럽트에서도 호출될 수 있다는 점이다. 그래서 이럴 경우에는 자신의 인터럽트인가를 확인하는 기능을 인터럽트 함수 안에 꼭 넣어야 하고, 자신의 인터럽트가 아니면 신속히 처리를 종료해야 한다. 그래야 다른 디바이스의 인터럽트 함수가 신속하게 호출될 수 있기 때문이다. 그런데 이렇게 동일한 인터럽트에 여러 가지 인터럽트 서비스 루틴이 호출되는 구조가 되면 디바이스 드라이버 호출에 응답하는 속도가 늦어진다. 왜냐하면 어떤 디바이스가 해당 디바이스를 사용하는지 몰라 커널이 등록된 인터럽트 서비스 함수를 모두 호출하기 때문이다. 그래서 open() 함수나 close() 함수에서 인터럽트 서비스 함수의 등록과 해제가 이루어지도록 해야한다. 그러나 같은 디바이스 파일을 사용하는 두 개 이상의 응용프로그램에서 다른 시점에 인터럽트 서비스 함수를 등록하고 해제한다면 동일한 인터럽트 서비스 루틴이 이중으로 등록을 시도한다. 이 경우 이중으로 등록을 시도하는 것은 리눅스 커널에서 처리할 수 있으나 등록 함수가 호출된 이후 에러가 반환되는 것까지 처리하지는 못한다. 디바이스 드라이버 프로그래머는 이 에러에 대하여 어떻게 처리할 것인가를 정해야 한다. 또한 응용 프로그램 중 먼저 해제를 시도하는 쪽에서 인터럽트 서비스 함수를 제거하므로 다른 쪽 응용프로그램에서 사용하는 디바이스 드라이버에서는 인터럽트 서비스 함수가 동작하지 않는다. 이런 문제는 모듈의 초기화나 해제 단계에서 처리하면 된다. 

     

    결론적으로 인터럽트 서비스 함수의 등록과 해제 시점은 시스템의 상황에 맞게 처리해야 하지만 PC 같은 범용 시스템에 사용되는 디바이스 드라이버를 만든다면 open() 함수와 close() g함수에서 처리해야 하고, 임베디드와 같은 특정 목적의 시스템에서는 모듈의 등록과 해제 시에 처리하는 것이 좋다.

     

    인터럽트 공유

    위에서 "이중으로 등록을 시도하는 것은 리눅스 커널에서 처리할 수 있으나"라고 했다. 인터럽트 서비스 함수를 등록하는 request_irq() 함수는 중복된 인터럽트 서비스를 구별하기 위해 flags 매개변수에 IRQF_SHARED 값을 포함하고 있는지를 먼저 확인한다. 만약 flags 매개변수에 IRQF_SHARED 값이 포함되어 있지 않으면 인터럽트 번호 하나에 대해 인터럽트 서비스 함수를 하나만 등록할 수 있다. 반대로 flags 매개변수에 IRQF_SHARED 값이 포함되어 있다면 request_irq() 함수는 dev 매개변수 값이 0이 아닌지 확인하여 만약 0이면 등록을 거부하고, 0이 아니면 동일한 값으로 등록된 인터럽트 서비스 함수가 있는지 확인한다. 동일한 값으로 등록된 경우가 아니라면 인터럽트 서비스 함수를 정식으로 등록한다. 

     

    또한 위에서 "자신의 인터럽트인가를 확인하는 기능을 인터럽트 함수 안에 꼭 넣어야 하고"라고 했다. 그렇기 때문에 인터럽트 서비스 함수를 작성할 때는 인터럽트 체크함수를 만들어 dev매개변수로 부터 받은값을 이용하여 인터럽트 서비스 함수가 관리하는 디바이스와 인터럽트를 발생시킨 디바이스가 서로 일치하는가를 확인한다.

     

    인터럽트 금지와 해제(수정 중..)

    인터럽트는 프로세서의 상태와 관계없이 주변 장치가 인터럽트 발생 조건을 만족하면 발생한다. 그래서 현재 인터럽트가 발생하여 인터럽트 서비스 함수가 동작하고 있더라도 또 다른 인터럽트가 발생할 수 있다. 이런 경우 일차적인 처리는 인터럽트 처리 우선 순위에 따른다. 우선 순위가 높은 인터럽트가 처리되고 있을 때에는 우선 순위가 낮은 인터럽트가 발생하더라도 곧바로 호출되자 않고 대기한다. 우선 순위가 높은 인터럽트 처리가 모두 끝나서 인터럽트 조건이 해제되면 대기하고 있던 우선순위가 낮은 인터럽트를 처리하기 시작한다. 인터럽트가 처리되고 도중에 동급이나 상위 우선 순위의 인터럽트가 발생되면 현재 수행중인 인터럽트 서비스는 중지되고 다른 인터럽트 서비스 함수가 호출된다.

     

    하드웨어에 따라 해당 인터럽트 처리를 수행하는 도중에 인터럽트 수행이 중지되면 곤란한 디바이스가 있다. 데이터의 입출력 처리를 위해서 특정 시점을 놓치면 동작에 문제가 생길 수 있는 디바이스가 그예인데, 이때 일반적인 처리를 수행하는 함수에서도 인터럽트와 연관된 데이터를 처리할 때는 해당 데이터를 처리하는 동안 인터럽트가 발생하지 않도록 주의해야한다.

     

    이러한 데이터 처리 오류를 막기 위해 인터럽트의 발생을 필히 막야야 한다. 이런 상황을 두가지 경우로 나누어 볼 수 있다. 첫번째는 인터럽트 서비스 함수가 동작중에 다른 인터럽트가 발생하지 못하게 막는 경우고, 두번째는 일반적인 함수 수행중에 데이터 처리를 보호하기 위해 인터럽트를 강제로 막는 경우다.

     

    인터럽트 서비스 함수를 수행하는 동안 다른 인터럽트가 발생하지 않도록 인터럽트 서비스 함수를 등록하면서 처리하는 방법을 사용한다. 즉 인터럽트 서비스를 등록하는 request_irq() 함수의 flags 매개변수에  SA_INTERRUPT or IRQF_DISABLED를 포함 시킨다. 하지만 커널 4.19.115 버전은 인터럽트가 발생하면 해당 CPU의 인터럽트 라인을 비활성화한 후 인터럽트 핸들러를 처리하는 구조로 동작한다. 따라서, 인터럽트 핸들러가 처리 중에 인터럽트를 비활성화하는 부분은 고려하지 않아도 된다.

     

     

     

    seqlock_t 구조체

    인터럽트 함수와 디바이스 드라이버간의 데이터 공유를 할때 2가지 방법을 소개했다. 그 중 전역 변수를 사용하여 데이터를 공유하는 방법은 인터럽트를 금지하는 루틴에 걸려있는 시간이 길거나 사용 빈도가 많을 경우 전반적인 시스템의 효율이 떨어지고, 간혹 빠른 처리를 요구하는 하드웨어일 경우에는 인터럽트 처리에 문제를 일으킬 수 있다. 처리 루틴을 수행하는 중에 전역 변수가 인터럽트 서비스 함수에서 변경될 수 도 있기 때문에 예기치 않았던 결과가 발생할 수 있다.

    이런 경우를 미연에 방지하기 위해서 seqlock_t 구조체를 제공한다. reader보다 writer가 먼저다. 우선 writer는 다른 writer가 없으면 항상 락을 잡글 수 있다. reader는 락에 아무런 영향을 미칠 수 없다. 대기중인 writer가 있으면 reader는 락을 소유한 writer가 락을 풀기전까지 루프(스핀락)를 돈다.

     

    블록킹 I/O

    블록킹 I/O는 디바이스 드라이버가 응용 프로그램에서 요구한 처리(데이터 읽기/쓰기)를 수행하기 위해 하드웨어가 준비될때 까지 프로세서를 잠시 동안 잠들게(sleep)하는 방식으로 여러 프로세스가 동작하는 리눅스의 효율을 높인다.

     

    응용 프로그램에서 디바이스 파일을 열 때 블록킹 I/O 모드는 기본적인 선택 사항이다. open() 함수의 두번째 매개변수에 O_NDELAY를 사용하면 디바이스 파일에 read() 함수나 write() 함수를 수행했을 때 프로세스를 잠들지 않게 한다. 비록 디바이스 파일을 블록킹 I/O 모드로 열었어도(O_NDELAY 사용 X) 디바이스 드라이버가 블록킹 I/O를 구현 하지 않는다면 프로세스는 잠들지 않는다.

     

     

    잠들거나 깨우기 위한 조건을 관리하거나 여러 프로세스의 접근을 관리하기 위해 디바이스 드라이버는 대기 큐라는 것을 사용한다. 대기 큐 변수는 wait_queue_head_t 구조체로 선언한다. 이 대기 큐는 필요한 조건마다 선언하고 사용한다. 예를 들어, 읽기 조건에 의해 잠든 구조가 필요하면 읽기 처리용 대기 큐를 만들어야 하고, 쓰기 위한 조건에 의해 잠드는 구조가 필요하면 쓰기 처리용 대기 큐를 만들어야 한다.

     

    보통 대기 큐의 선언은 전역 변수로 처리한다. 대기 큐 처리 함수가 프로세스나 인터럽트간의 상호 충돌 문제를 해결하고 있기 때문이다.

     

    interruptible_sleep_on() 함수는 매개변수에 적용된 대기 큐 변수에 현재 프로세스의 정보를 등록하고 프로세스를 재운다. 그리고 스케줄러는 즉시 다른 프로세스에게 프로세서를 할당한다. interruptible_sleep_on() 함수에 의해 잠든 프로세스를 깨우려면 wake_up_interruptible() 함수를 사용한다. 이 함수는 깨우려는 프로세스가 등록된 대기 큐 변수를 매개 변수에 대입한다. wake_up_interruptible() 함수는 대기 큐에 등록된 프로세스를 모두 차례대로 다음 스케줄에서 수행될 대상으로 만든다.

     

     

    응용 프로그램 하나가 디바이스 파일을 하나만 다루지 않는다. 대부분 여러 디바이스 파일을 이용해 목적한 바를 처리한다. 그리고 이런 경우에 하나의 디바이스 파일에서 블록킹 I/O가 발생하면 프로세스의 흐름이 뭠처 블록되어 있는 동안 다른 디바이스 파일은 처리하지 못하게 된다. 이러한 문제점을 해결하기 위해 리눅스에서는 입출력 다중화라는 개념을 사용한다.

    입출력 다중화

    여러 개의 시리얼 통신 포트에서 데이터가 수신되기를 기다리고 있다가 먼저 수신된 데이터를 처리하는 프로그램을 작성해야 하는 경우, 하나의 시리얼 포트에서 read() 함수를 사용하여 수신 데이터를 읽으려고 했을 때 디바이스 드라이버를 수신한 데이터가 없다면 프로세스를 재운다. 이때 프로세스가 블록되면 프로그램이 멈추고, 다른 시리얼 통신 포트에서 데이터가 도착하더라도 처리하지 못한다.

     

    이와 같은 문제를 해결하기 위해 리눅스에서는 시리얼 포트 하나당 프로그램을 하나씩 작성하여 동작시키거나 혹은 하나의 프로그램, 즉 프로세스에 여러 개의 스레드를 발생시켜 각각의 시리얼 포트의 입출력을 처리하도록 하는 방법을 쓴다. 그러나 만약에 하나의 프로세스로 여러 개의 디바이스를 처리할 수 있다면 좀더 간단하게 해결할 수 있지 않을까? 이런 생각에서 출발한 개념이 바로 입출력 다중화 방식이며,, 리눅스에서는 디바이스와 연관된 디바이스 파일을 동시에 여러 개 처리하기 위해 select() 함수와 poll 함수를 지원한다.

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    인터럽트 후반부처리

    왜씀?: 인터럽트 핸들러에서 인터럽트를 처리하는 동안 인터럽트는 비활성화 상태가 된다. 핸들러가 실행되는 시간이 길어지는 만큼 시스템의 응답성이 떨어지고 자칫 인터럽트를 놓칠 수도 있다.

     

    종류: IRQ스레드, Soft IRQ, 태스크릿, 워크 큐

     

    softirq

    인터럽트 발생 빈도가 아주 높은 인터럽트 후반부를 처리할 때 사용 인터럽트 발생 빈도가 높다는 말은 하드웨어로 부터 어떠한 값이 빨리 들어온다는 말이고 이를 빨리 해결하기 위해서는 하드웨어 인터럽트가 발생하지않는한 선점되지 않는 softirq를 사용하는 것이 좋다

     

    태스크릿

    태스크릿은 프로세스별로 존재하는 tasklet_vec tasklet_hi_vec 연결 리스트가 존재하기 때문에 서로 다른 cpu에서 

    IRQ스레드

    IRQ스레드는 RT프로세스로 구동하기 때문에 우선순위가 높다. 발생 빈도가 높은 경우 사용하면 좋다.

     

    워크큐

    긴시간

    고려해야할점: 인터럽트를 시스템에서 처리하는 방식과 인터럽트가 얼마나 자주 발생하는지

     

    1. 인터럽트가 1초에 수 백번 발생하는 디바이스의 경우

    IRQ 스레드 방식과 워크큐 방식은 적합하지 않다. IRQ 스레드는 RT프로세스(우선순위가 높은 프로세스, MAX_USER_RT_PRIO/2 = 50, 0~100까지 RT고 101~139까지가 normal이다)로 구동하므로 다른 프로세스들이 선점 스케줄링을 할 수 없다. IRQ 스레드 핸들러 실행 시간이 조금이라도 길어지면 다른 프로세스들이 실행을 못하고 대기해야 하므로 시스템 반응속도가 느려질 수 있다. 만약 IRQ 스레드 방식을 적용해야 한다면 IRQ 스레드 핸들러 함수 실행 시간이 매우 짧아야 한다. 예를 들면, IRQ 스레드 핸들러 함수에 printk() 함수와 같이 커널 로그를 출력하는 코드도 되도록 입력하면 안된다.

     

    또한 워크큐를 실행하는 워커 스레드는 일반 프로세스로 프로세스 우선 순위가 높지 않다. 인터럽트 발생 횟수 만큼 워크 핸들러가 실행을 못할 수 있다.

     

    따라서 인터럽트가 자주 발생하는 디바이스는 Sort IRQ나 태스크릿 방식을 적용하는 것이 바람직하다.

    IRQ 스레드는 우선순위가 높아서 다른 프로세스 실행이 잘 안됨/ 워크큐는 우선순위가 낮아서 현재? 프로세스 실행이 잘 안됨

     

    2. 현재 개발 중인 시스템은 인터럽트 개수가 200개 정도인 경우

    1초에 인터럽트가 수 백번 발생하는 경우를 제외하곤 IRQ 스레드 방식을 적용하면 별 문제가 없다. 그런데 인터럽트 개수 만큼 IRQ 스레드를 생성하면 기본으로 프로세스를 관리할 때 필요한 태스크 디스크립터와 같은 메모리 공간을 써야한다. 만약 현재 개발 중인 시스템 RAM 용량이 8G 이상이면 별 문제가 되지 않는다.

     

    인터럽트 발생 빈도가 낮고 빠른 시간에 인터럽트 후반부를 처리하지 않아도 될 경우 워크큐 기법을 적용하는 것도 좋다.

     

    인터럽트 후반부 단계에서 인터럽트 처리를 최적화하도록 설계를 잘하려면 먼저 커널이 인터럽트를 처리하는 세부 동작과 인터럽트 후반부 기법들의 세부 구현 방식을 잘 알고 있어야 한다.

     

    hard IRQ

    hard IRQ는 인터럽트 핸들러다. 하드웨어 인터럽트 컨텍스트에 있는 코드를 실행하는 동안은 실행을 중단할 수 없다(선점, sleep이 안된다)

     

    soft IRQ

     

    softIRQ가 처리되는 경우 

    1. 하드웨어 인터럽트 핸들러의 동작이 끝났을 때

    2. bottom-half를 활성화 할 때

    3. raise_softirq()로 softirq를 펜딩시켰을 때

    4. __do_softirq()에서 오랜시간 수행되었거나 최대 처리 반복횟수를 초과했을때

    5. 강제 스레드화 설정일 경우

    3번 4번 5번은 ksoftirqd 태스크에서 처리된다.

     

    하드웨어 인터럽트가 발생하지 않는 한 softirq는 바로 실행된다.

    소프트웨어 인터럽트 컨텍스트에서 수행된다. 소프트웨어 인터럽트 컨텍스트는 sleep하거나 다른 소프트웨어 인터럽트에 의해 선점되지 않으며 오직 hard IRQ의 인터럽트 핸들러에 의해서만 선점될 수 있다. 따라서 하드웨어 인터럽트 컨텍스트와 소프트웨어 인터럽트 컨텍스트 사이에 데이터를 공유하는 경우에는 반드시 동기화가 필요하다.

     

    태스크릿

    태스크릿이 처리되는 경우

    1. raise_softirq_irqoff()로 펜딩시켰을 때(tasklet_schedule() 함수에 있음)

    2. __do_softirq()에서 오랜시간 수행되었거나 최대 처리 반복횟수를 초과했을때

    softirq와 마찬가지로

     

    스킵

     

     

    softirq와 마찬가지로 소프트웨어 인터럽트 컨텍스트에서 수행되기 때문에 소프트웨어 인터럽트 컨텍스트는 sleep하거나 다른 소프트웨어 인터럽트에 의해 선점되지 않으며 오직 hard IRQ의 인터럽트 핸들러에 의해서만 선점 될 수 있다. 태스크릿은 softirq를 기반으로 만들어진 후반부 처리 방식이다. softirq에 비해 인터페이스가 더 간단하고, 락 사용 제한이 더 유연하다. 거의 softirq보다 태스크릿을 사용한다. softirq는 실행횟수가 빈번하고 병렬처리가 필요한 경우에만 사용한다.

     

     

    irq 스레드

     

     

     

    워크큐

    워크큐는 지연 작업을 커널 스레드 형태로 처리한다.

    프로세스 컨텍스트에서 실행되며 휴먼 상태로 전환이 가능한 유일한 후반부 처리 방식이다.(irq 스레드는 아직모름) 

    워크큐를 사용할 것인지 softirq나 태스크릿을 사용할 것인지 결정하는 일은 지연되는 작업이 휴면 상태로 전환이 필요한 경우라면 워크큐를 사용한다. 지연되는 작업이 휴면 상태로 전환될 필요가 없으면 softirq나 태스크릿을 사용한다. 

     


    softirq

    동작과정

    Soft IRQ 서비스 핸들러는 컴파일시에 정적으로 할당된다. 즉 부팅시 open_softirq() 함수를 호출해 Soft IRQ 서비스를 등록한다. 부팅과정에서 등록하기 때문에 한번만 한다.

     

    __handle_irq_event_percpu() 함수에서 하드웨어 인터럽트 핸들러를 호출하고 하드웨어 인터럽트 핸들러에서 raise_softirq() 함수를 호출해 Soft IRQ 서비스를 요청한다.(실행하는것 아님)

     

    하드웨어 인터럽트 핸들러가 끝나면 __handle_domain_irq() 함수로 돌아와 irq_exit() 함수를 실행하고 위에서 raise_softirq() 함수로 서비스 요청 여부를 확인한다. 요청이 없으면 종료된다.

     

    요청이 있는데 강제 스레드화 되어있으면 ksoftirqd 태스크를 통해서 __do_softirq() 함수를 실행하고 강제 스레드화 되어 있지 않으면 __do_softirq() 함수를 실행한다.(이 부분에서 그대로 인터럽트 컨텍스트 상태에서 실행할 지 아니면 프로세스 컨텍스트 상태에서 실행할 지 결정된다.)

     

    __do_softirq() 함수에서 open_softirq() 함수로 등록한 Soft IRQ 서비스 핸들러를 호출하여 실행한다.

     

    호출하다가 특정 시간이 걸리거나 특정 횟수가 넘어가면 위 처럼 강제 스레드화를시켜 ksoftirqd 태스크로 실행한다.(이 부분 또한 특정 시간 횟수 안에 Soft IRQ 서비스 핸들러가 끝나면 인터럽트 컨텍스트 상태에서 실행된 것이고, 시간이나 횟수가 넘어가면 ksoftirq 태스크로 실행되기 때문에 인터럽트 컨텍스트 상태에서 프로세스 컨택스트 상태로 넘어가게 된다.)

     

    ksoftirqd 프로세스가 깨어나면 위에서 마무리 못한 SOFT_IRQ 서비스 핸들러를 실행한다.

     

     

    결론

    인터럽트 컨텍스트가 아닐때도 ksoftirqd 스레드를 통해서 Soft IRQ 서비스를 요청할 수 있다.

     

    인터럽트 발생 빈도가 아주 높거나, Soft IRQ 서비스 함수를 바로 실행해야되는 경우는 softirq 방식을 사용한다.

     

    비활성화 했던 현재 cpu의 bottom-half를 활성화할 때도 __do_softirq() 함수(Soft IRQ 서비스 핸들러)를 처리한다.

     

    태스크릿

    동작과정

    태스크릿은 Soft IRQ 서비스 중 하나다. 그래서 Soft IRQ 서비스 실행 흐름도와 거의 비슷하다.

     

    Soft IRQ 서비스 핸들러(tasklet_action() 함수)는 컴파일시에 정적으로 할당된다. 그래서 부팅시 open_softirq() 함수를 호출해 Soft IRQ 서비스(tasklet_action() 함수)를 등록한다. 부팅과정에서 등록하기 때문에 한번만 한다.

     

    태스크릿 핸들러 구조체는 동적, 정적으로 만들 수 있다. 정적이라면 DECLARE_TASKLET() 매크로를 또는 구조체에 변수를 선언하고 안에다가 값을 때려 넣는다. 동적으로 만들려면 tasklet_init() 함수를 이용한다. 

     

    만들어진 태스크릿 핸들러 구조체를 매개변수로 tasklet_schedule() 함수를 호출한다.

     

    참고로 tasklet_schdule() 함수는 하드웨어 인터럽트 핸들러에서 호출한다.

     

    __handle_irq_event_percpu() 함수에서 하드웨어 인터럽트 핸들러를 호출하고 하드웨어 인터럽트 핸들러에 있는 tasklet_schedule() 함수에서 raise_softirq_irqoff() 함수를 호출하여 태스크릿 서비스를 요청한다.(실행하는것 아님)

     

    하드웨어 인터럽트 핸들러가 끝나면 __handle_domain_irq() 함수로 돌아와 irq_exit() 함수를 실행하고 위에서 raise_softirq() 함수로 서비스 요청 여부를 확인한다. 요청이 없으면 종료된다.

     

    요청이 있는데 강제 스레드화 되어있으면 ksoftirqd 태스크를 통해서 __do_softirq() 함수를 실행하고 강제 스레드화 되어 있지 않으면 __do_softirq() 함수를 실행한다.(이 부분에서 그대로 인터럽트 컨텍스트 상태에서 실행할 지 아니면 프로세스 컨텍스트 상태에서 실행할 지 결정된다.)

     

    __do_softirq() 함수에서 open_softirq() 함수로 등록한 Soft IRQ 서비스를 우선순위 별로 실행 중 Soft IRQ 서비스 중 하나인 태스크릿 서비스인 tasklet_action() 함수를 호출하여 실행한다. tasklet_action() 함수에서 태스크릿 핸들러를 수행한다.

     

    호출하다가 특정 시간이 걸리거나 특정 횟수가 넘어가면 위 처럼 강제 스레드화를시켜 ksoftirqd 태스크로 실행한다.(이 부분 또한 특정 시간 횟수 안에 Soft IRQ 서비스 핸들러가 끝나면 인터럽트 컨텍스트 상태에서 실행된 것이고, 시간이나 횟수가 넘어가면 ksoftirq 태스크로 실행되기 때문에 인터럽트 컨텍스트 상태에서 프로세스 컨택스트 상태로 넘어가게 된다.)

     

    ksoftirqd 프로세스가 깨어나면 위에서 마무리 못한 SOFT_IRQ 서비스 핸들러를 실행한다.

     

    결론

    softirq나 태스크릿이나 똑같다. Soft IRQ 서비스 종류에 태스크릿이라는게 존재하는것 뿐이다.

    Soft IRQ 서비스에는 태스크릿은 2개가 존재하고 서비스 중에서 우선순위가 가장 높은 서비스에  HI_TASKLET가 존재하고 서비스 중에서 보통 우선순위인 TASKLET_SOFTIRQ가 존재한다. 이 태스크릿은 스케줄링된다. 

    태스크릿도 Soft IRQ 서비스의 한 종류이기 때문에 휴면상태가 될 수 없다. 

     

    태스크릿도 마찬가지로 인터럽트 컨텍스트 뿐만아니라 프로세스 컨텍스트에서 동작이 가능하다.

     

    IRQ 스레드

    IRQ 스레드는 인터럽트 핸들러에서 처리하면 오래 걸리는 일을 수행하는 프로세스다.

    동작과정

    __handle_irq_event_percpu() 함수에서 하드웨어 인터럽트 핸들러를 호출하고 IRQ_WAKE_THREAD를 리턴하면 해당 IRQ 스레드를 깨운다.

     

    IRQ 스레드를 깨우면 스케줄러는 우선 순위를 고려한 후 IRQ 스레드를 실행한다.

     

    결론

    인터럽트 컨텍스트에서 동작하지 않고 프로세스 컨텍스트에서 IRQ 스레드를 동작하는것이다.

     

    IRQ 스레드는 RT 프로세스로 구동하므로 다른 프로세스들이 선점 스케줄링을 할 수 없다. 왜? RT 프로세스는 우선순위가 높으니까. IRQ 스레드 방식을 적용해야 한다면 IRQ 스레드 핸들러 함수 실행 시간이 매우 짧아야 한다. IRQ 스레드 핸들러 실행 시간이 조금이라도 길어지면 다른 프로세스들이 실행을 못하고 대기해야 하므로 시스템 반응속도가 느려질 수 있다. 예를 들면, IRQ 스레드 핸들러 함수에 printk() 함수와 같이 커널 로그를 출력하는 코드도 되도록 입력하면 안된다.

    워크큐

    동작과정

    일단 간단하게 말하면 작업이라고 하는 단위로 워크큐에 요청하면 워커 스레드가 이를 꺼내 처리한다. 워커는 처리할 작업이 없으면 idle 상태로 진입했다가 작업이 도착하면 깨어나 동작한다.

     

    워커 스레드 동작 과정

     

    create_worker() 함수로 워커 스레드를 만든다.

     

    휴면 상태에서 다른 드라이버가 자신을 깨워주기 기다린다.

     

    워크를 워크큐에 큐잉하고 워커 스레드를 깨우면 스레드 핸들러인 worker_thread() 함수가 실행된다.

     

    워커 스레드가 필요가 없으면 소멸된다.

     

    __handle_irq_event_percpu() 함수에서 하드웨어 인터럽트 핸들러를 호출하고 하드웨어 인터럽트 핸들러에서 __queue_work() 함수로 워크를 워크큐에 큐잉한다. (근데 schedule_~~ () 함수로 호출 하는듯?)

     

    __queue_work() 함수의 insert_work() 함수를 실행하면 wake_up_worker() 함수를 호출하여 워커 스레드를 꺠운다.

     

    워커 스레드가 깨어나면 스레드 핸들러인 worker_thread() 함수가 수행된다. 

     

    worker_thread() 함수의 process_one_work() 함수에서 워크 핸들러를 실행한다.

     

    http://rousalome.egloos.com/9981559

     

    [리눅스커널]워크큐(Workqueue): 워크큐 주요 개념 알아보기

    이번 소절에서는 워크큐를 이루는 주요 개념을 소개합니다.  -   워크  -   워커스레드  -   워커풀  -   풀워크큐 먼저 워크큐의 기본 실행 단위인 워크를 배워볼까요? 워크란 워크는 워크�

    rousalome.egloos.com

     

    워크를 만들기 위해서는 DECLARE_WORK() 함수로 워크큐에 등록될 작업 구조체 변수를 선언하고 초기화한다. 또는 작업구조체를 정적으로 생성하고 INIT_WORK() 함수로 초기화한다.

     

    이제 schedule_work() 함수로 워크를 워크큐에 넣어야 한다. schedule_work() 함수에서  queue_work() 함수를 호출하고 queue_work() 함수에서 queue_work_on() 함수를 호출하고 queue_work_on() 함수에서 __queue_work() 함수를 호출하고 __queue_work() 함수에서  insert_work() 함수를 호출하여 워크큐에 워크를 큐잉한다. insert_work() 함수에서 wake_up_worker() 함수를 호출하고 wake_up_worker() 함수에서 wake_up_process() 함수를 호출하고 wake_up_process() 함수에서 try_to_wake_up() 함수를 호출하여 태스크(워커)를 깨운다.

     

    schedule_work() 함수로 전달하는 워크는 시스템 워크큐에 큐잉된다. 

     

    생각해보니 워크큐가 이미 있다. 워크큐는 초반에 생성한다. 워크큐는 총 7개 있고 workqueue_init_early() 함수로 생성한다.(init_workqueue() 함수는 뭐지??;;)

     

    그럼 워커는 뭐고 워커 스레드는 뭘까? 워커는 태스크 ,워커 스레드는 프로세스라고 생각하면 된다. 워커 스레드를 생성하기 위해서는 우선 워커를 생성해야 한다.

     

    워커는 create_worker() 함수를 호출하여 생성한다. 이때 worker_thread() 함수를 워커의 task에 대입한다.

     

    하지만 기본적으로 워크큐 자료구조를 초기화할 때 부팅 시 워커를 생성한다. 그래서 마찬가지로 워크큐를 생성한 workqueue_init() 함수에서 각 워크 풀 별로 create_worker() 함수를 호출하여 워커도 생성했다. 

     

     

    워크를 워크큐에 넣었어.. 그럼 워크스레드가 워크 큐에있는 워크를 실행해야겠지??? 

     

    그럼 worker_thread 함수가 워크큐에있는 워크를

    워커랑 워크큐의 관계???

     

    그럼 스케줄링에 의해서 워커(워커 스레드)가 수행되게 되고 워커 스레드의 핸들러인 worker_thread() 함수가 실행되어

     

     

     

     

    (여기서 워크큐는 이미 구현이 되어있다 컴파일 시점에서??)

     

     

     

     

     

     

     

     

     

    워크를 연결시킬 워크리스트를 관리할 워커풀를 담을 워커큐는 cpu당 4개가있다???

     

    결론

     

     

     

     

     


     

     

    커널스레드 생성과정

     

    커널 스레드 생성 과정은 2단계다

     

    1. kthreadd 프로세스에게 커널 스레드 생성 요청

    kthread_create() 함수를 호출한다 매개변수로 스레드 핸들러, 매개 변수, 커널 스레드 이름일 지정한다. (threadd 프로세스가 실행되면 ktreadd() 함수가 실행되는것 처럼 스레드 핸들러를 지정해줘야 된다.)

    ktrhead_create() 함수를 호출하면 kthread_create_on_node() 함수가 호출되고 kthread_create_on_node() 함수는 __kthread_create_on_node() 함수를 호출하여 태스크를 생성하고 생성된 태스크를 kthread_create_list에 추가한다. 마지막으로 wak_up_process() 함수를 호출하여 threadd프로세스를 깨운다.

     

    2. kthreadd 프로세스가 커널 스레드 생성

    kthreadd 프로세스를 깨우면 kthreadd 프로세스 스레드 핸들러인 kthreadd() 함수가 실행된다. kthreadd() 함수에서는 kthread_create_list를 확인해 프로세스 생성 요청을 확인하고 요청이 없으면 schedule() 함수를 호출해 스스로 휴면 상태로 진입한다. kthread_create_list가 비어있지 않으면 create_kthread() 함수를 호출한다. create_kthread() 함수에서 kernel_thread() 함수를 호출하고 kernel_thread() 함수에서 _do_fork() 함수를 호출해 프로세스를 생성하는 일을 시작한다.

     

    자세한 동작흐름

    http://rousalome.egloos.com/10011978

     

    [리눅스커널] 프로세스: 커널 스레드는 어떻게 생성할까?

    이어서 커널 스레드를 생성하는 과정에서 호출되는 함수를 소개하고 세부 코드를 분석하겠습니다. 커널 스레드가 생성되는 과정은 크게 2단계로 나눌 수 있습니다. 1) 1단계: kthreadd 프로세스에��

    rousalome.egloos.com

     

     

     

    근데 kthreadd 프로세스가 커널 프로세스인데 kthreadd 프로세스가 커널프로세스를 생성하면 kthread 프로세스는 언제 생긴걸까..

     

    바로 시스템 부팅과정에서 대부분의 커널 스레드를 생성한다.

     

    그럼 커널 스레드는 어떤것이 있을까?

     

    1. kthreadd 프로세스

    위에서 설명한 kthreadd 프로세스다. 모든 커널 스레드의 부모 프로세스다. 스레드 핸들러는 kthreadd() 함수다.

     

    2. 워커 스레드

    워크큐에서 요청된 워크를 실행하는 프로세스다. 스레드 핸들러는 worker_thread() 함수다.

     

    3. ksoftirqd 프로세스

    Soft IRQ를 실행하는 프로세스다. 스레드 핸들러는 run_ksoftirq() 함수다.

     

    4. IRQ 스레드

     

     

    softirq

    동작과정

    softirq는 인터럽트 컨텍스트와 프로세스 컨텍스트에서 동작할 수 있다. 

     

    인터럽트 컨텍스트 환경에서 동작한다는 말은 인터럽트가 발생된 상태에서 실행이 된다는 뜻이다.

    프로세스 컨텍스트 환경에서 동작한다는 말은 태스크처럼 하나의 작업으로써 스케줄링되면서 실행이 된다는 뜻이다.

     

    첫번째로 인터럽트 컨텍스트 환경에서 동작하는 방법을 알아보자.

     

    인터럽트가 발생하면 여러 함수를 거쳐 하드웨어 인터럽트 핸들러를 처리하고 다시 돌아온다. 

    즉, 인터럽트가 발생하면 여러 함수를 거쳐 __handle_irq_event_percpu() 함수에 도착하게 되고 __handle_irq_event_percpu() 함수에서 하드웨어 인터럽트 핸들러를 호출하여 수행을 한다. 하드웨어 인터럽트 핸들러가 끝나면 거쳐간 여러 함수로 되돌가게 되고 그 중 __handle_domain_irq() 함수로 돌아와 irq_exit() 함수를 실행하게 되고 invoke_softirq() 함수를 실행하여 강제 스레드화가 되어있는지를 확인한다. 여기서 강제 스레드화가 되어있으면 위에서 설명한 인터럽트 컨텍스트 환경에서 벗어나고 프로세스 컨텍스트 환경에서 동작하는 흐름을 타게 된다. 일단 강제 스레드화가 안되어있다고 가정하면 do_softirq_own_stack() 함수를 실행하고 __do_softirq() 함수를 실행해서 등록된 Soft IRQ 서비스를 실행하게 된다. 여기서 __do_softirq() 함수에서 Soft IRQ 서비스를 실행하는 도중에 지정한 횟수나 시간을 초과하게되면 스레드로써 인터럽트 컨텍스트 환경에서 동작하게된다.

     

    두번째로 프로세스 컨텍스트 환경에서 동작하는 방법을 알아보자.

     

    __handle_irq_event_percpu() 함수에서 하드웨어 인터럽트 핸들러를 호출하고 하드웨어 인터럽트 핸들러에서 raise_softirq() 함수를 호출하고 raise_softirq() 함수에서 raise_softirq_irqoff() 함수를 호출하여 wakeup_softirq() 함수를 호출한다. wakeup_softirq() 함수를 호출하면 ksoftirqd 프로세스를 깨우게 되고 일정한 시간이 지나면 ksoftirqd 프로세스의 핸들러인 run_ksoftirqd() 함수가 실행하게 된다. run_ksoftirqd() 함수는 __do_softirq() 함수를 호출하고 Soft IRQ 서비스를 실행하게 된다. 위에서와 같이 Soft IRQ 서비스를 실행하는 도중에 지정한 횟수나 시간을 초과하게 되면 다시 일정한 시간이 지난 후 ksoftirqd 프로세스 핸들러인 run_ksoftirqd() 함수가 실행는 식으로 반복된다.

     

    또한 인터럽트 컨텍스트 환경에서 강제 스레드화 된다고 했었는데 강제 스레드화 되면 똑같이 invoke_softirq() 함수에서 wakeup_softirq() 함수를 호출하여 ksoftirq 태스크를 깨우고 일정한 시간이 지나면 run_ksoftirq() 함수를 수행되고 똑같이 __do_softirq() 함수를 실행되어 Soft IRQ 서비스를 수행하게 된다. 

     

     

    Soft IRQ 서비스 핸들러는 컴파일시에 정적으로 할당된다. 즉 부팅시 open_softirq() 함수를 호출해 Soft IRQ 서비스를 등록한다. 부팅과정에서 등록하기 때문에 한번만 한다.(따로 추가할 수 도있다.)

     

    결론

    인터럽트 컨텍스트가 아닐때도 ksoftirqd 스레드를 통해서 Soft IRQ 서비스를 실행할 수 있다.

     

    인터럽트 발생 빈도가 아주 높거나, Soft IRQ 서비스 함수를 바로 실행해야되는 경우는 softirq 방식을 사용한다.

     

    비활성화 했던 현재 cpu의 bottom-half를 활성화할 때도 __do_softirq() 함수(Soft IRQ 서비스 핸들러)를 처리한다

     

     

     

     

    태스크릿 

    태스크릿은 softirq방식으로 동작하기 때문에 softirq 방식을 따른다.

    하지만 Soft irq 서비스는 open_softirq() 함수로 등록되지만 태스크릿은 tasklet_action이라는 함수가 등록되고tasklet_action() 함수가 tasklet_vec 리스트에 있는 태스크릿 서비스를 실행하게 된다.

    그래서 __do_softirq() 함수가 실행되면 softirq방식에서는 등록한 Soft irq 서비스가 수행되지만 태스크릿에서는 tasklet_action() 함수가 실행되고 tasklet_action 함수가 tasklet_vec 리스트에있는 태스크릿 서비스를 실행하게 되는 것이다.

     

    IRQ 스레드

    IRQ 스레드는 softirq방식과 유사하다 하지만 인터럽트 컨텍스트 환경에서 실행이되지 않고 하드웨어 인터럽트 핸들러에서 해당 태스크를 깨워 프로세스 컨텍스트 환경에서 동작하도록 되어있다.

    http://rousalome.egloos.com/10014325

     

    [리눅스커널] 인터럽트 후반부 처리: IRQ 스레드의 전체 실행 흐름 정리

    지금까지 IRQ 스레드 핸들인 irq_thread() 함수에서 irq_thread_fn() 함수를 호출해 IRQ 스레드 핸들러 함수를 호출하는 과정을 살펴봤습니다. 이번에는 배운 내용을 정리하는 차원으로 IRQ 스레드 전체 실

    rousalome.egloos.com

     

     

    워크큐

    워크큐는 프로세스 컨텍스트에서 실행이된다.

     

    워크 실행 흐름은 간단하게 다음과 같다.

     

    1. 워크 초기화

    INIT_WORK() 함수나 DECLARE_WORK() 함수로 워크를 생성한다.

     

    2. 워크 큐잉

    schedule_work() -> ~~ -> __queue_work() -> insert_work() 함수에서 list_add_tail() 함수를 호출하여 넘겨 받은 워크를 리스트에 추가하고 wake_up_worker() 함수를 호출하여 워커 스레드를 꺠운다.

     

    3. 워크 실행

    깨어난 워커 스레드는 일정한 시간이 지난 후 워커 스레드가 수행되고 worker_thread() 함수를 호출하고 process_one_work() 함수로 워크 서비스 함수를 실행한다.

     

    http://egloos.zum.com/studyfoss/v/5626173

     

    [Linux] concurrency managed workqueue (cmwq)

    Linux: 3.3-rc1workqueue는 특정 작업을 별도의 process context에서 실행하고 싶은 경우 사용하는커널 API로 커널 내의 다양한 위치에서 널리 사용된다. 이는 일종의 thread pool의개념으로 볼 수 있으며, workqu

    egloos.zum.com

     

    내일 할일: 위 사이트가서 해석하니까 그럼 worker_thread() 함수는 워크마다 독립적인가?? 아니면 worker_thread()가 스케줄링을 해서 실행하는가?? 알아볼것

     

     

     

    workqueue_struct 구조체는 총 7가지가 있고, workqueue_struct 구조체마다 cpu 갯수만큼 pool_workqueue 구조체가 있다. 그럼 워크리스트는 통 28개가 있는건가?

     

    워크를 만들으면 워크큐에 큐잉해야된다. 정확하게는 work_strcut 구조체를

    7가지 workqueue_struct 구조체 중 하나를 고르고

    그 workqueue_struct 구조체가 가리키는 (CPU 갯수 만큼 존재하는)pool_workqueue 구조체가 가리키는 worker_pool 구조체가 가리키는 리스트에 넣어야하는것 같다.

     

     

    보통태스크릿과 워크큐를 많이 사용한다.

     

     

     

     

    댓글

Designed by Tistory.