반응형

1. 개요

인터럽트(interrupt)라는 말은 ‘방해하다’라는 의미로 무언가를 수행하는 동안에 방해를 받는다는 뜻이다. 프래그래밍 관점에서 보면 그림 1과 같이 어떤 프로세스가 수행되고 있는 동안에 다른 서비스 처리 루틴이 끼어들어 프로세스 수행을 방해하는 것을 말한다. 인터럽트가 발생한 시점에서는 잠시 동안 정지했다가 미리 정해진 인터럽트 처리 루틴을 실행하기 시작한다. 이후 인터럽트 처리 루틴은 프로세서의 현재 상태를 저장하고 인터럽트 처리를 수행한다. 인터럽트 처리가 끝나게 되면 기존에 처리되고 있던 루틴이 정상적으로 수행된다.

 

  예를 들어 키보드의 인터럽트 처리과정을 살펴보면 사용자가 PC 키보드의 특정 키를 누르면 키보드 컨트롤러는 PC의 프로세서에 해당 키와 관련된 인터럽트를 발생시킨다. PC의 프로세서는 인터럽트가 발생하면 현재 수행중이던 처리 내용을 중단시키고 키보드 인터럽트 처리 루틴을 수행한 다음 프로세스의 현재상태를 메모리에 저장시킨다. 이후 키보드에 의해 전달된 키 값을 저장하게 된다. 이후 키보드에 대한 인터럽트 처리 루틴이 완료되면 메모리 공간에 저장해 두었던 프로세스 상태 값을 이용하여 다시 원래의 상태로 복귀하게 된다.

인터럽트는 프로세서 자체에서 발생하는 인터럽트와 프로세서 외부에서 발생하는 인터럽트로 나눌 수 있다. 프로세서 자체에서 발생하는 인터럽트는 시스템의 RESET이나 소프트웨어 인터럽트 또는 0으로 나눌 경우에 발생하는 오류 인터럽트, 버스 에러 인터럽트와 같은 것들이 있다. 이러한 인터럽트는 대부분 리눅스 커널 내부에서 특별하게 제어된다. 반면, 프로세서 외부에서 발생되는 인터럽트는 IRQ(interrupt request)라 불리는 인터럽트 요청을 통해 디바이스 드라이버에서 제어된다. 프로세스 간 스케줄링을 담당하고 있는 타이머 인터럽트 역시 이러한 인터럽트의 한 종류라고 볼 수 있으며 PC의 경우에는 시리얼 인터럽트, 키보드 인터럽트, 프린터 인터럽트 등이 이에 해당된다. 본 문서에서는 디바이스 드라이버에서 제어되는 IRQ를 통한 인터럽트에 대해 설명하며 이를 위한 프로그래밍으로 IRQ 인터럽트 처리 발생시간 출력을 수행한다.

 

2. IRQ 인터럽트 처리

2.1. 인터럽트 처리 과정

IRQ 인터럽트를 처리하는 방식은 i386계열, ARM 계열 등 각 프로세서마다 다르다. 따라서 리눅스 커널에서는 각 프로세서 계열의 IRQ 인터럽트를 동일하게 처리하기 위해 모두 do_IRQ()를 호출하여 처리하고 있다. do_IRQ()는 IRQ 처리만 담당하며 irq_desc라는 전역 변수에 등록되어 있는 인터럽트 서비스 함수를 호출하는 구조로 되어 있다. 인터럽트 처리가 필요한 경우 처리하고자 하는 IRQ 번호에 해당하는 인터럽트 서비스 함수를 irq_desc 전역 변수에 등록하면 된다. 만약 더 이상의 처리가 필요없다면 해당 인터럽트 서비스 함수를 제거하면 된다.

그림 2는 IRQ 인터럽트의 서비스 처리 흐름을 나타낸 것이다. 그림에서 보듯이 IRQ 인터럽트 서비스를 처리하기 위해서는 request_irq()를 이용하여 처리하고자 하는 IRQ 번호와 서비스 함수 주소를 등록한다. 인터럽트가 발생하면 커널은 각 프로세서 아키텍처마다 발생된 인터럽트의 IRQ 번호를 획득하여 do_IRQ()를 호출하게 된다. 호출된 do_IRQ()는 irq_desc 전역 변수에 해당 IRQ 번호와 일치하는 인터럽트 서비스 함수를 찾아보고, 등록된 함수가 있으면 호출하고 없으면 무시하게 된다. 이때 request_irq()를 이용한 등록과정에서 참조된 데이터의 주소나 값은 인터럽트 서비스 함수의 파라미터인 dev_id에 전달된다. 이러한 인터럽트 서비스 함수는 특정 하드웨어에서 인터럽트 상황이 발생되기를 기다리고 있다가 하드웨어가 인터럽트를 발생하면 리눅스 커널 내에서 자신에게 정의된 인터럽트 처리 내용을 수행한다. 이후 인터럽트 처리가 더 이상 필요 없을 경우 free_irq()를 이용하여 irq_desc 전역변수에 등록된 해당 인터럽트 서비스 함수를 제거한다.

 


2.2. 인터럽트 서비스 함수의 형식

디바이스 드라이버에서 인터럽트를 처리하기 위해서는 인터럽트가 발생했을 때 처리할 루틴을 담은 인터럽트 서비스 함수가 필요하다. 인터럽트 서비스 함수는 리눅스 커널 버전이 2.6인지 혹은 2.4인지에 따라 다른데 두 함수의 가장 큰 차이점은 반환값이 있느냐 없느냐의 차이다. 2.4 및 2.6 버전의 인터럽트 서비스 함수의 형태 및 각 파라미터는 다음과 같다.

// 커널 2.4

void int_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

}

// 커널 2.6

irqreturn_t int_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

return IRQ_HANDLED;

}

 

• irq

첫 번째 파라미터로 해당 인터럽트 서비스 함수가 수행되었을 때 처리 루틴에서 참고할 수 있도록 넘겨주는 인터럽트 번호로 흔히 IRQ 번호라고 한다.

• dev_id

인터럽트 ID 값을 뜻하는 dev_id에는 인터럽트 공유에 사용되거나 인터럽트 서비스 함수를 수행할 때 필요한 정보가 저장된 메모리의 첫 번째 주소를 가지고 있다. 이 값은 인터럽트 서비스 함수를 등록할 때 지정된 값이 그대로 전달된다.

• regs

인터럽트가 발생한 당시의 레지스터에 저장되어 있던 값들은 regs 변수에 전달된다. regs 변수에는 프로세서의 모든 레지스터 값이 들어갈 수 있는데 대부분의 경우에는 사용되지 않는다.

2.3. 인터럽트 서비스 함수의 등록 및 해제

디바이스 드라이버에서 인터럽트를 처리하기 위해서는 가장 먼저 해당 인터럽트 서비스 함수를 등록해야 한다. 인터럽트를 등록하기 위해 사용되는 함수는 request_irq()이다. 이 함수의 기본형은 다음과 같다.

// 커널 2.4

int request_irq (unsigned int irq, void (*handler)(int, void *, struct pt_regs *), unsigned long frags, const char *device, void* dev_id);

 

// 커널 2.6

int request_irq (unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs), unsigned long frags, const char *device, void* dev_id);

이 등록 함수의 파라미터인 irq에 해당하는 인터럽트가 발생핬을 때 또 다른 파라미터인 handler로 지정된 인터럽트 서비스 함수를 호출하도록 커널에 등록하게 된다. frags라는 파라미터는 인터럽트 공유나 빠른 인터럽트 같은 처리 속성을 지정하며 SA_INTERRUPT(다른 인터럽트 허가 하지 않음), SA_SHIRQ(동일한 인터럽트 번호 공유), SA_SAMPLE_RANDOM(랜덤값 처리에 영향을 줌) 등의 값들을 비트 OR 연산을 통해 사용하게 된다. 4번째 파라미터인 device는 인터럽트에 대한 소유자를 나타내며, 마지막 dev_id 파라미터는 인터럽트가 공유되었을 때 인터럽트에 대한 구분 인자로 사용되기도 하고, 인터럽트 서비스 함수가 참조하는 데이터에 대한 주소를 지정하기도 한다.

등록된 인터럽트 서비스 함수를 해제하기 위해서는 free_irq() 함수를 사용한다. 이 함수는 request_irq() 함수와 달리 커널 2.4/2.6 버전과 무관하게 형식이 동일하다. 이 함수는 request_irq()함수로 등록되어 있던 인터럽트 서비스 함수를 제거하는데, 커널에서 제거하고자 하는 함수를 찾을 수 있도록 irq와, dev_id 파라미터를 전달한다. free_irq() 함수의 기본형은 다음과 같다.

// 커널 2.4, 2.6

void free_irq(unsigned int irq, void *dev_id);

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

인터럽트 서비스 함수는 프로세스 상의 context와 별개로 동작한다. 즉, 인터럽트 서비스 함수가 수행될 때 어떤 디바이스 드라이버가 연관되어 있는지는 인터럽트 서비스 함수 내에서 알 수가 없다. 인터럽트의 특성상 임의의 시점에서 수행되기 때문에 비록 인터럽트 서비스 함수와 관련있는 디바이스 드라이버가 동작하지 않는다 할지라도 인터럽트는 처리된다. 따라서 인터럽트 서비스 함수와 디바이스 드라이버 함수간에는 데이터를 공유하는 방법이 필요한데, 그 중 하나는 전역변수를 이용하는 방법이고 다른 하나는 dev_id 파라미터를 이용하는 방법이다. 각 방법에 대한 설명은 다음과 같다.

 2.4.1. 전역변수를 이용하여 데이터를 공유하는 경우

인터럽트가 발생했을 때마다 숫자 하나를 증가시키기 위해 전역 변수를 사용하는 예제는 다음과 같은 형식을 사용하게 된다. 이러한 방식은 인터럽트 하나에 인터럽스 서비스 함수 하나가 사용되어 동작하는 경우이지만, 동일한 인터럽트 서비스 함수로 여러 디바이스를 제어할 경우에는 사용할 수 없다. 응용 프로그램이 디바이스 드라이버의 디바이스 파일을 열면 int_open()가 호출된다. 이 함수는 request_irq()를 이용하여 count_interrupt()를 등록한다. SA_INTRERRUPT는 다른 인터럽트를 허가하지 않도록 하기 위해 설정된 것이며 int_release()는 디바이스 파일이 닫힐 때 호출된다.

static unsigned long int_count=0;

 

irqreturn_t count_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

int_count++;

return IRQ_HANDLED;

}

 

int int_open(struct inode *inode, struct file *filp)

{

if(!request_irq(XXX_IRQ, count_interrupt, SA_INTERRUPT, "test", NULL))

{

int_count=0;

}

return 0;

}

 

int int_release(struct inode *inode, struct file *filp)

{

free_irq(XXX_IRQ,NULL);

printk("int_count %d\n", int_count)l

return 0;

}

 

2.4.2. dev_id를 이용하여 데이터를 공유하는 경우

여러 디바이스를 하나의 인터럽트 서비스 함수에서 사용할 수 있도록 전역변수를 사용하지 않고, 디바이스의 다른 정보를 전달하는 보편적인 방법인 dev_id를 이용하여 데이터를 공유하는 방법이 있다. 이러한 방법은 다중 프로세스 환경에 적합한 방식이며 다음은 관련 예제를 나타낸 것이다. 이 예제는 open()나 release() 호출 시 전달되는 파라미터 filp의 private_data에 해당 디바이스 파일을 처리하는데 필요한 구조체의 메모리를 할당하고, 이를 request_irq()와 free_irq()의 dev_id 파라미터에 지정하는 것이다. 여기서 사용된 enable_hardware_int()와 disable_hardware_int()는 커널에서 제공된 함수가 아니라 인터럽트를 하드웨어적으로 제어하는 디바이스 고유의 함수를 설명하기 위해 임의로 사용한 것이다. 즉, 인터럽트를 하드웨어적으로 허가하고 금지하는 것을 처리하기 위해 이 함수들을 이용하여 의도적으로 수행하도록 한 것이다.

typedef struct

{

unsigned long ioaddress;

unsigned long count;

} __attribute__ ((packed)) R_INT_INFO;

 

irqreturn_t count_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

R_INT_INFO *ptrInfo;

ptrInfo = (R_INT_INFO *) dev_id;

prink("INT READ I/O %02X\n", inb(prtInfo->ioaddress));

ptrInfo->count++;

return IRQ_HANDLED;

}

 

int int_open (struct inode *inode, struct file *filp)

{

R_INT_INFO *ptrInfo;

ptrInfo = kmalloc(sizeof(R_INT_INFO), GFP_KERNEL);

filp->private_data = ptrInfo;

switch (MINOR(inode->i_rdev))

{

case 0 : ptrInfo -> ioaddress = 0x378; break;

case 1 : ptrInfo -> ioaddress = 0x278; break;

}

if(!request_irq(XXX_IRQ, count_interrupt, SA_INTERRUPT, "test", ptrInfo))

{

enable_hardware_int(ptrInfo);

}

return 0;

}

 

int int_release (struct inode *inode, struct file *filp)

{

R_INT_INFO *ptrInfo = (R_INT_INFO *) filp->private_data;

disable_hardware_int(ptrInfo);

free_irq(XXX_IRQ, ptrInfo);

kfree(ptrInfo);

return 0;

}

 

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

리눅스 커널에서는 필요 시 언제든지 인터럽트 서비스 함수를 등록 혹은 해제할 수 있다. 그래서 아주 간단한 디바이스 드라이버라면 모듈 초기화 함수가 호출될 때 인터럽트 서비스 함수를 커널에 등록하고, 모듈 제거시 호출되는 함수에서 인터럽트 서비스 함수를 제거하게 된다.

휴대폰 같은 임베디드 시스템에서는 인터럽트 하나에 디바이스 하나를 연결한다. 하지만 PC 같은 경우는 하드웨어 제작사가 다양하기 때문에 동일한 인터럽트를 사용하는 전혀 다른 디바이스들이 존재한다. 여기에 인터럽트의 수도 제한이 있기 때문에 효율적으로 관리할 필요가 있다. 따라서 이러한 점을 극복하기 위해 인터럽트를 여러 디바이스가 공유할 수 있는 방법이 제공되고 있다. request_irq()의 파라미터 중 frags에 SA_SHIRQ를 설정하면 인터럽트 공유가 가능하며, 결과적으로 같은 인터럽트 번호에 대해 여러 디바이스 드라이버 서비스 함수를 등록할 수 있게 된다. 하지만 이렇게 동일한 인터럽트에 여러 가지 인터럽트 서비스 루틴이 호출되면 어떠한 디바이스가 해당 디바이스를 사용하는지 모르기에 모든 인터럽트 서비스 함수를 호출하여 응답속도가 늦어지게 된다.

하나의 응용 프로그램만이 디바이스 파일을 사용한다면 인터럽트 서비스 함수 등록과 해제를 open()와 close() 시점에 넣어도 상관이 없다. 하지만 같은 디바이스 파일을 사용하는 두 개 이상의 응용 프로그램에서 다른 시점에 인터럽트 서비스 함수를 등록하고 해제한다면 동일한 인터럽트 서비스 루틴이 이중으로 등록을 시도하게 된다. 이 경우 이중으로 등록을 시도하는 것은 리눅스 커널에서 처리 가능하나 등록 함수가 호출된 이후에 에러가 반환되는 것까지 처리를 하지 못한다. 또한, 응용 프로그램 중 먼저 해제를 시도한 쪽에서 인터럽트 서비스 함수를 제거하므로 다른 쪽 응용프로그램에서 사용하는 디바이스 드라이버에서는 인터럽트 서비스 함수가 동작하지 않는다. 따라서 이러한 문제들은 모듈의 초기화나 해제 단계에서 처리하면 된다.

결론적으로 말하면 인터럽트 서비스 함수의 등록과 해제 시점은 시스템의 상황에 따라 처리해야 하지만, PC 같은 경우는 open()와 close()에서 처리해야 하고, 임베디드 시스템과 같은 특정 목적의 시스템에서는 모듈의 등록과 해제 시에 처리하는 것이 좋다.

2.6. 인터럽트 공유

PCI(peripheral component interface) 디바이스 드라이버나 PC의 시리얼 디바이스들은 인터럽트 하나를 공유하여 여러 하드웨어가 동작하는 경우가 있다. 리눅스 커널은 이러한 상황을 처리할 수 있도록 인터럽트 서비스 함수를 등록할 때 같은 인터럽트 번호일 경우 다른 인터럽트 서비스 함수를 등록할 수 있도록 지원한다. 다음과 같은 형태로 등록 및 해제 시 함수가 사용된다. 인터럽트 서비스 함수를 등록하는 request_irq()는 중복된 인터럽트 서비스를 구별하기 위해 frags 파라미터에 SA_SHIRQ 포함하는 지 확인한 후, 포함되어 있으면 request_irq()는 dev_id 값이 0이 아닌지 확인한다. 0일 경우 등록을 거부하고 0이 아니면 동일한 값으로 등록된 인터럽트 서비스 함수가 있는 지 확인하게 된다. 동일한 값으로 등록된 경우가 아니라면 해당 인터럽트 서비스 함수를 등록한다. 예를 들어, 통신 디바이스로 여러 개의 시리얼 디바이스가 하나의 인터럽트를 공유하고 있다고 가정한다면(COM1, COM2) 실제 인터럽트 서비스 루틴은 같고, 대상이 되는 디바이스만 다르므로 같은 인터럽트 번호에 동일한 인터럽트 함수가 여러개 등록된다. dev_id 파라미터로 구별하지 않고 인터럽트 서비스 함수로 중복을 방지하면 동일한 인터럽트 함수가 여러 개 등록되는 경우에는 처리가 불가능하므로 dev_id 파라미터로 구별하게 된다.

//등록할 때

request_irq(XXX_IRQ, xxx_int_service_func, SA_INTERRUPT|SA_SHIRQ, PRNINT_DEV_NAME, 0x01)

//해제할 때

free_irq(XXX_IRQ, 0x01);

 

인터럽트를 공유할 때는 같은 인터럽트에 대해 여러 인터럽트 서비스 함수가 동작할 경우가 생길 수도 있으므로 인터럽트 서비스 함수는 필히 자신이 관리하는 디바이스에서 인터럽트가 발생했는지를 확인하는 과정이 있어야 한다.

 

2.7. 인터럽트 금지와 해제

인터럽트는 프로세서의 상태와 관계없이 주변 장치가 인터럽트 발생을 위한 조건을 만족시키면 발생하게 된다. 따라서 현재 인터럽트가 발생하여 인터럽트 서비스 함수가 동작하고 있더라도 또 다른 인터럽트가 발생 가능하다. 이러한 경우 인터럽트 처리 우선순위에 따라 수행이 된다. 예를 들어 우선순위가 높은 인터럽트가 처리되고 있을 때는 우선순위가 낮은 인터럽트가 발생하더라도 바로 호출되지 않고 대기하게 된다. 인터럽트가 처리되고 도중에 동급이나 높은 우선순위의 인터럽트가 발생하면 현재 수행중인 인터럽트 서비스는 중지되고 다른 인터럽트 서비스 함수가 호출된다. 하드웨어에 따라 이러한 우선순위로 인한 인터럽트 수행이 중지되면 곤란한 디바이스가 존재한다. 따라서 이를 방지하기 위해서는 인터럽트 서비스 함수가 동작중에 다른 인터럽트가 발생하지 못하게 막거나 혹은 일반적인 함수 수행중에 데이터 처리를 보호하기 위해 인터럽트를 강제로 막을 수 있다.

인터럽트 서비스 함수를 수행하는 동안에 다른 인터럽트가 발생하지 않도록 인터럽트 서비스 함수를 등록하면서 처리하는 방법을 사용하는 경우, request_irq()의 frags 파라미터로 SA_INTERRUPT를 포함시킨다. 이로써 인터럽트가 발생했을 때 SA_INTERRUPT 값이 포함된 인터럽트 서비스 함수는 커널이 프로세서의 인터럽트를 금지시킨 후 호출한다. 또한, 인터럽트가 두 개가 동시에 발생하면 커널은 SA_INTERRUPT가 설정된 서비스 함수를 먼저 수행하고 모든 처리가 끝난 후에 SA_INTERRUPT가 포함되지 않은 인터럽트 서비스 루틴을 수행한다. 그래서 SA_INTERRUPT가 포함된 인터럽트 서비스 함수를 빠른 인터럽트 핸들러라고도 하며 포함되지 않은 인터럽트 서비스 함수를 느린 인터럽트 핸들러라고도 한다.

또한, 특정 인터럽트 번호에 대해 인터럽트를 강제로 금지하고자 할 때는 다음과 같은 함수를 사용하게 된다.

//특정 인터럽트 관련 헤더 파일

#include <asm/irq.h>

//인터럽트 금지

void disable_irq(int irq)

//인터럽트 허가

void enable_irq(int irq)

반면 특정 인터럽트 번호가 아닌 프로세서 전체에서 발생하는 인터럽트를 금지하고 해제하고자 할 때는 다음과 같은 함수를 사용하게 된다.

//프로세서 전체 인터럽트 관련 헤더 파일

#include <asm/system.h>

//프로세서의 인터럽트 처리를 금지

local_irq_disable(void)

//프로세서의 인터럽트 처리를 허가

local_irq_enable(void)

//현재의 프로세스의 상태를 저장

local_save_flags(unsigned long frags)

//현재의 프로세스의 상태를 복구

local_irq_restore(unsigned long frags)

3. IRQ 인터럽트 처리 발생시간 출력 프로그램

이번 장에서는 프린터 포트를 이용하여 인터럽트를 디바이스 드라이버에서 실행 할 것이다. 실험을 시작하기에 앞서 프린터 포트의 하드웨어적인 특성, 그중에서도 인터럽트 특징에 대하여 알아야 한다. 프린터 포트 신호 중 ACK 신호가 있다. 이 신호는 프린터가 컴퓨터에게 데이터를 수신하였다는 확인 신호를 보내주는데 사용되는 신호이다. 컴퓨터에 데이터를 전송하고 데이터가 정상적으로 전송되었는지를 확인하려면 이 ACK 신호를 확인하여야 한다. 이 ACK 신호는 프린터 포트 10번 핀에 연결되어 있다. 이 핀의 신호는 다른 핀들과 다르게 인터럽트를 발생시키며 전압 상태가 5V에서 0V로 떨어지면, 즉 하강 edge가 발생하면 인터럽트가 발생한다. 이번 장에서는 인터럽트를 확인하기 위하여 10번 핀과 GND를 연결시켜 인터럽트를 발생시키고 그에 따라 특정한 동작을 하는 디바이스 드라이버를 구현할 것이다

.

3.1. 소스 프로그램의 구조 설명

예제로 사용되는 소스는 int_dev.c와 int_app.c 두 가지이다. int_dev.c는 프린터 포트를 제어하고 인터럽트를 구현하는 디바이스 드라이버 소스이며, int_app.c는 디바이스 드라이버를 동작시켜 제어를 구현하는 응용 프로그램 소스이다.

디바이스 파일은 주 번호가 240, 부 번호가 0인 /dev/intdev를 사용한다. 인터럽트 관련 디바이스 드라이버를 구현할 때 커널 2.4는 인터럽트 서비스 함수형이 void 형이며 인터럽트 발생 시간을 기록할 때 jiffies를 직접 사용한다. 시스템의 정확한 시간을 측정하기위해서는 소프트웨어적이 아닌 하드웨어적인 것을 이용해야한다. 시스템 타이머는 주기마다 인터럽트를 발생시키는데 이 인터럽트를 시스템 타이머 인터럽트라고 하며 이 인터럽트가 한번 발생될 때마다 jiffies의 값을 1씩 증가시키게 된다. 시스템 타이머 주기는 하드웨어에 따라 다를 수 있으며 보통 100Hz를 사용한다.

이번 장에서의 예제를 통하여 인터럽트 서비스 함수 등록과 해제 그리고 인터럽트 서비스 함수를 어떻게 구현하는지를 알 수 있다. 이를 구현하기 위하여 응용 프로그램은 다음과 같은 순서를 동작하도록 한다.

(1) int_app 실행 파일은 write()함수를 이용하여 LED를 켠 후, read()함수를 이용하여 인터럽트 발생여부를 무한 루프를 돌면서 확인한다.

(2) 10번 핀에 GND를 접촉시켜 인터럽트를 발생시키고 read()함수는 인터럽트가 발생하는 횟수를 반환한다. 이 반환값을 이용하여 무한 루프를 탈출하고 인터럽트 발생 시간을 출력한다. 이때 chattering에 의한 인터럽트 잔여 발생 횟수를 얻기 위하여 1초의 지연을 준다.

(3) LED를 write()함수를 이용하여 1초 간격으로 5회 점멸시킨 후 종료한다.

 

3.2. 인터럽트를 디바이스 드라이버에 구현

다음은 int_app.c와 int_dev.c의 실제 코드에 대한 설명이며 소스코드는 [1]을 통해 참고하길 바란다. 인자의 선언과 같은 기본적인 부분의 설명은 생략하도록 한다.

• [int_app.c] 25행: int_app.c는 open() 함수를 이용하여 /dev/intdev(#define으로 선언되어 있음) 디바이스 파일을 연다. 디바이스 파일에 읽기와 쓰기가 가능하고 함수 호출 중에 블록되지 않도록 하기 위해서 O_RDWR과 O_NDELAY를 OR한 값을 open() 함수의 매개변수 값으로 사용한다.

• [int_dev.c] 60~72행: 응용프로그램(app.c파일에 의한 프로그램)이 /dev/intdev 디바이스 파일을 열면 file.operation 구조체에 의하여 디바이스 드라이버의 int_open() 함수가 호출된다. int_open()( 함수는 request_irp() 함수를 이용하여 프린터 포트의 인터럽트 서비스 하수인 int_interrupt() 함수를 등록한다. 이때 등록된 사항은 인터럽트를 빠르게 표현하기 위하여 SA_INTERRUPT 옵션을 주고, 인터럽트 서비스에서 사용할 데이터는 따로 정의하지 않으므로 마지막 인자는 NULL을 설정한다. 또한 인터럽트의 사용 디바이스 이름을 주기 위해 “intdev”dls INT_DEV_NAME 문자열 상수값을 사용한다. 이 문자열 상수는 /prox/interrupts에서 인터럽트의 디바이스 드라이버 이름으로도 쓰인다. 인터럽트 서비스 함수가 정상적으로 등록되면 0이 반환된다. 반환값이 0이면 프린터 포트의 입력 핀 중 10번 입력이 변화할 때 인터럽트가 발생하도록 outb() 함수를 이용하여 0x037A번지에 0x10 값을 써넣는다. 인터럽트가 발생했을 때 시간을 기록하는 구조체 변수 intbuffer를 초기화하기 위해 int_clear()함수를 호출한다.

• [int_dev.c] 39~49행: 인터럽트가 발생하면 인터럽트 서비스 함수는 R_INT_INFO 구조체 변수인 intbuffer에 인터럽트가 발생한 시점의 시간값을 기록하고 발생 횟수를 intcount에 기록한다. 이 값을 초기화하는 int_clear() 함수를 선언하는데, 이 함수는 intcount를 0으로 만들고 intbuffer 배열 버퍼의 time 필드를 모두 0으로 초기화한다.

• [int_app.c] 29~31행: open()함수가 정상적으로 수행되면 프로그램의 시작을 알리는 메시지인 "start..."를 출력하고 write()함수를 이용하여 LED를 켠다.

• [int_dev.c] 92~108행: 디바이스 파일에 write()함수를 이용하여 LED 값을 전달하면 int_dev의 int_wirte()함수가 호출된다. int_write()함수는 두 가지를 처리한다. 첫 번째는 인터럽트가 발생했을 때의 시간이 기록되는 intbuffer의 내용을 모두 0으로 설정하고, 발생된 횟수를 기록하는 intcount 변소를 초기화하는 int_clear()함수를 호출한다. int_app에서 디바이스 파일을 열고 write()함수를 호출하면 LED를 동작시키기도 하지만, 정확한 인터럽트의 기록 시점을 측정할 수 있다.

• [int_app.c] 33~39행: 인터럽트가 발생되기를 기다리기 위해 “wait...input" 메시지를 출력하고, read()함수를 이용하여 인터럽트가 발생된 횟수를 얻어온다. int_dev.c 디바이스 드라이버에서 read()함수를 이용하여 데이터를 읽어올 때는 일반적인 read()함수와는 조금 다른 형식으로 구현되어 있다. 일반적인 read()함수라면 연속된 스트림의 데이터를 읽어오는 형태를 가정하지만, int_dev.c로 구현된 디바이스 드라이버의 read()함수는 인터럽트의 발생 시간과 횟수를 얻어오기 위해 일정한 규칙을 가진다. read()함수에 전달해야 하는 버퍼의 크기는 R_INT_INFO 구조체의 배수가 되어야 하고, INT_BUFF_MAX를 넘을 수 없다. read() 함수에서 반환된 크기도 R_INT_INFO 구조체의 배수가 된다. 그래서 read() 함수에서 반환한 값을 R_INT_INFO 구조체의 크기로 나누면 발생한 인터럽트 횟수가 된다. 원천적으로 read() 함수가 실패할 경우에 반환된 값이 0보다 작은 값이 발생할 수 있지만, 이 예제에서는 편의상 무시한다. 반환 값을 R_INT_INFO 구조체의 크기로 나누었을 때 0이 아니면 인터럽트가 발생한 것으로 인식하고, 무한 루프를 탈출한다.

• [int_dev.c] 50~58행: 인터럽트가 발생하면 인터럽트 서비스 함수인 int_interrupt()함수가 호출된다. 이 함수의 호출 시점은 프로세스 문맥 밖에서 발생하기 때문에 응용 프로그램에서 연 디바이스 파일에 대한 정보가 전달될 수 없다. 응용 프로그램에서 연 디바이스 파일과 같은 정보나 응용 프로그램의 프로세스별 정보를 전달하기 위해서는 인터럽트 서비스 함수의 매개변수 중 void*dev_id와 request_irq()함수의 마지막 매개변수 인자를 이용해야 한다. request_irq()함수의 마지막 인자값은 인터럽트가 발생하여 인터럽트 서비스 함수가 호출되었을 때 dev_id 매개변수에 전달된다. 이 예제에서는 이에 대한 내용은 구현하지 않고, 전역 변수를 이용하는 방법으로 인터럽트에서 발생한 데이터 처리한다. int_interrupt()함수는 가장 먼저 intcount 변수를 조사하여 intbuffer에 발생 시간을 기록할 수 있는 공간이 있는지를 확인한다. 기록 공간이 있으면 get_jiffies_64() 함수를 이용하여 현재 시간을 intbuffer 배열 버퍼의 time 필드에 기록한다. 그리고 intcount 변수값을 1 증가시킨다. 정상적인 인터럽트 함수가 처리되었으므로 IRQ_HANDLED를 반환값으로 전달하며 함수를 종료한다.

• [int_dev.c] 74~91행: int_app에서 디바이스 파일에서 read()함수를 이용하여 데이터를 읽으면 디바이스 드라이버의 int_read()함수가 호출된다. int_read()함수는 count 값을 R_INT_INFO 구조체 크기로 나누어 요청된 데이터 크기를 산출하고, 그 크기가 현재 기록된 인터럽트 횟수인 intcount와 비교하여 전달할 수 있는 횟수로 재조정한다. intbuffer 버퍼는 커널 메모리 공간이므로 호출한 프로세스 공간에 내용을 전달하기 위해 put_user() 함수를 사용하여 내용을 복사한다. 반환값으로 복사된 크기를 반환한다.

• [int_app.c] 41~50행: read()함수로 입력을 학인하여 발생된 인터럽트 횟수가 1이상이면 sleep()함수를 이용하여 1초간 잠시 잠들어서 채터링에 의해 발생할 수 있는 인터럽트를 모두 대기시킨다. 1초 후에 read()함수를 이용하여 발생된 시간 정보를 R_INT_INFO 구조체 변수인 intbuffer에 읽어 와서 내용을 출력한다.

• [int_app.c]52~62행: LED를 write()함수를 이용하여 5회간 2초 간격으로 점멸시키고, close() 함수를 이용하여 디바이스 파일을 닫는다.

• [int_dev.c] 109~116행: int_app에서 close()함수를 이용하여 디바이스 파일을 닫으면 디바이스 드라이버의 int_release()함수가 호출된다. int_release()함수는 프린터포트의 제어 러지스터인 0x037A에 0x00을 써넣어 프린터 포트의 10핀 입력이 변화하더라도 인터럽트가 발생하지 않게 한다. 그리고 free_irq()함수를 이용하여 등록된 프린터 인터럽트 7번에 대한 서비스 하수를 제거한다.

 

3.3. 소스 프로그램 실행 방법

예제를 테스트 하기 위해 프린터 포트에 LED 하나와 금속 클립 두 개를 준비한다. 준비된 LED의 +쪽 다르를 프린터 포트의 2번 핀에 연결하고, ㅡ쪽 단자는 18번 핀에 연결한다. 금속 클립은 각각을 10번 핀과 25번 핀에 꽂는다. 프린터 포트의 핀은 그림 3과 같다. 금속 클립들이 서로 닿지 않게 을 프린터 포트에 연결한 후 손으로 눌러서 서로 붙을 수 있게 한다.

 

이 예제를 실행하기 위해서는 디바이스 파일을 만들어야 한다. 다음과 같은 명령을 사용하여 만든다. 또한 디바이스 드라이버를 모듈로 만들기 위해 makefile을 이용하여 컴파일한다.

[root@] # mknod /dev/intdev v 240 0

[root@] # make

[root@] # gcc -o int_app int_app.c

[root@] # ./int_app

LED가 켜지고 입력을 기다리는 메시지가 출력된다. 금속 클립을 접촉시키면 1초 후에 LED가 꺼지고 인터럽트가 발생한 시간의 tick 값을 출력한 후 LED가 1초 주기로 5회 점멸한다. 그림 4의 수행 결과를 보면 클립을 잠시 붙였다 떨어뜨려도 채터링 현상에 의해 여러 번의 인터럽트가 발생함을 알 수 있다. 수행이 끝나면 모듈을 제거한다.

 

[root@] # rmmod int_dev

4. 결론 및 고찰

본 문서에서는 리눅스 기반의 디바이스 드라이버의 인터럽트 처리를 구현하였다. 디바이스 드라이버에서의 인터럽트는 프로세서 자체적으로 발생하는 인터럽트와 프로세서 외부에서 발생하는 IRQ 인터럽트가 있는데 본 문서에서는 IRQ 인터럽트에 대해 소개하였으며 이를 프린터 포트에서 LED와 클립을 이용하여 인터럽트가 발생했을 시 인터럽트가 발생한 횟수와 인터럽트 발생한 시간을 출력하는 프로그램을 구현하였다.

참 고 문 헌

[1] 유영창, “리눅스 디바이스 드라이버,” 한빛미디어, 2004.

반응형

+ Recent posts