머리말
차단 및 비차단은 장치 액세스의 두 가지 기본 방법입니다. 이 두 가지 방법을 사용하여 드라이버는 차단 및 비차단 액세스를 유연하게 지원할 수 있습니다. 차단 및 비차단 드라이버를 작성할 때 대기 대기열이 자주 사용되므로 이 장에서는 대기 대기열에 대해 간략하게 소개합니다.
차단 및 비 차단
阻塞调用
이는 호출 결과가 반환되기 전에 현재 스레드가 일시 중단됨을 의미합니다. 함수는 결과가 나올 때까지 반환하지 않습니다. 어떤 사람들은 차단 호출을 동기 호출과 동일시할 수 있지만 실제로는 다릅니다. 동기 호출의 경우 현재 스레드는 많은 경우에 여전히 활성 상태이지만 논리적으로 현재 함수는 반환되지 않습니다.
non-blocking의 개념은 blocking에 해당하는데, 이는 결과를 즉시 얻을 수 없기 전에 함수가 현재 스레드를 차단하지 않고 즉시 반환한다는 것을 의미합니다. 개체가 차단 모드인지 여부는 함수가 호출을 차단하는지 여부와 강한 상관 관계가 있지만 일대일 대응은 없습니다. 차단 개체에 대한 비 차단 호출 방법이 있을 수 있으며 특정 API를 통해 상태를 폴링하고 차단을 피하기 위해 적절한 시간에 차단 기능을 호출할 수 있습니다. 비 차단 개체의 경우 특수 함수를 호출하면 차단 호출에 들어갈 수도 있습니다. 함수가 select()
이에 대한 예입니다. 다음은 select()
블록에 함수를 호출하는 예입니다.
void main()
{
FILE *fp;
struct fd_set fds;
struct timeval timeout={
4, 0}; //select()函数等待4s,4s后轮询
char buffer[256]={
0}; //256字节的缓冲区
fp = fopen(....); //打开文件
while(1)
{
FD_ZERO(&fds); //清空集合
FD_SET(fp, &fds); //同上
maxfdp=fp+1; //描述符最大值加1
switch(select(maxfdp, &fds, &fds, NULL, &timeout)) //select函数使用
{
case -1:
exit(-1);
break; //select()函数错误,退出程序
case 0:
break; //再次轮询
default:
if(FD_ISSET(fp, &fds)) //判断是否文件中有数据
{
read(fds, buffer, 256, ...); //接受文件数据
if(FD_ISSET(fp, &fds)) //测试文件是否可写
fwrite(fp, buffer...); //写入文件buffer清空
}
}
}
}
대기 대기열
이 섹션에서는 드라이버 프로그래밍에서 일반적으로 사용되는 대기 큐 메커니즘을 소개합니다. 이 메커니즘은 대기 프로세스를 일시적으로 잠들게 하고 대기 신호가 도착하면 대기 큐에 있는 프로세스를 깨워 실행을 계속합니다. 이 섹션에서는 대기 대기열의 내용에 대해 자세히 설명합니다.
대기 대기열 개요
Linux 드라이버에서 블로킹 프로세스는 等待队列(Wait Queue)
. 대기 큐는 매우 유용하기 때문에 Linux2.0 시대에 대기 큐 메커니즘이 도입되었습니다. Waiting Queue의 기본 데이터 구조는 one 双向链表
이며, 이 연결 목록에는 잠자는 프로세스가 저장됩니다. 대기 큐는 또한 프로세스 스케줄링 메커니즘과 밀접하게 통합되어 있으며 커널에서 비동기 이벤트 알림 메커니즘을 구현하는 데 사용할 수 있습니다. 대기 대기열을 사용하여 시스템 리소스에 대한 액세스를 동기화할 수 있습니다. 예를 들어, 한 작업이 완료되면 다른 작업을 수행할 수 없습니다.
커널에서 대기 큐는 특히 인터럽트 처리, 프로세스 동기화, 타이밍 및 기타 경우에 유용합니다. 대기 큐를 사용하여 차단된 프로세스를 깨울 수 있습니다. 큐를 기본 데이터 구조로 사용하고 프로세스 스케줄링 메커니즘과 밀접하게 통합되며 커널에서 비동기 이벤트 알림 메커니즘을 구현하고 시스템 리소스에 대한 액세스를 동기화하는 데 사용할 수 있습니다.
대기 큐 구현
플랫폼에 따라 제공하는 명령어 코드가 다르므로 대기 큐 구현도 다릅니다. Linux에서 대기 큐의 정의는 다음 코드에 표시됩니다.
struct __wait_queue_head{
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
이 구조의 각 멤버 변수는 아래에 자세히 소개되어 있습니다. 잠금 스핀 잠금의 기능은 매우 간단하며 연결 목록을 보호하는
1.lock自旋锁
데 사용됩니다 . task_list
Linked List에 요소를 추가하거나 삭제할 때 task_lsit
커널 내부에서 잠금이 잠기고 수정이 완료되면 잠금이 해제됩니다. 즉, 잠금 스핀 잠금은 task_lsit
AND 연산 중에 대기 큐에 대한 배타적 액세스를 실현합니다.
2.task_list变量
task_list
대기 중인 프로세스를 저장하는 데 사용되는 양방향 순환 연결 목록입니다.
대기 대기열 사용
Linux에서 대기 대기열 유형은 입니다 struct wait_queue_head_t
. 커널은 작동할 일련의 기능을 제공합니다 struct wait_queue_head_t
. 다음은 Waiting Queue의 작동 방법을 간략하게 소개합니다.
1.定义和初始化等待队列头
Linux에서 Waiting Queue를 정의하는 방법은 공통 구조를 정의하는 방법과 동일하며 정의 방법은 다음과 같다.
struct wait_queue_head_t wait;
Waiting Queue는 초기화를 거쳐야 사용할 수 있는데, init_waitqueue_head()
Waiting Queue를 초기화하는 함수로 코드 형식은 다음과 같다.
#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITALIZER(name)
2.定义等待队列
대기 큐를 정의하기 위해 Linux 커널에 매크로가 언급되어 있으며 이 매크로의 코드는 다음과 같습니다.
#define DECLARE_WAITQUEUE(name, tsk) \wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
이 매크로는 라는 대기 대기열을 정의하고 초기화하는 데 사용됩니다 name
.
3.添加和移除等待队列
Linux 커널은 대기열을 추가하고 제거하는 두 가지 기능을 제공하며 이 두 기능의 정의는 다음과 같습니다.
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
add_wait_queue()
이 함수는 대기 큐 헤드 q가 가리키는 대기 큐 연결 목록에 대기 큐 요소 wait를 추가하는 데 사용됩니다. 반대의 기능은 remove_wait_queue()
이 기능을 사용하여 대기 큐 q가 가리키는 대기 큐에서 대기 큐 요소를 삭제하는 것입니다.
4.等待事件
일부 매크로는 해당 이벤트를 기다리기 위해 Linux 커널에서 제공되며 이러한 매크로는 다음과 같이 정의됩니다.
#define wait_event(wq, condition)
#define wait_event_timeout(wq, condition, ret)
#define wait_event_interruptible(wq, condition, ret)
#define wait_event_interruptible_timeout(wq, condition, ret)
wait_event
매크로의 기능은condition
참이 될 때까지 대기 대기열에서 휴면하는 것입니다. 대기 기간 동안 프로세스는 변수가 참이 될TASK_UNINTERRUPTIBLE
때까지 휴면 상태가 됩니다.condition
프로세스가 깨어날 때마다 확인하는 값입니다condition
.wait_event_timeout
매크로도wait_event
비슷하지만 주어진 수면 시간이 음수이면 즉시 반환합니다. 수면 중에 깨면condition
남은 수면 시간을 반환 하고 그렇지 않으면 0을 반환하며 지정된 수면 시간에 도달하거나 초과할 때까지 계속 수면을 취합니다.wait_event_interruptible
매크로와 차이점은wait_event
이 매크로를 호출하는 대기 프로세스 동안 현재 프로세스가 상태로 설정된다는 것입니다TASK_INTERRUPTIBLE
. 깨어날 때마다 먼저condition
참인지 확인하고 참이면 반환하고, 그렇지 않으면 시그널에 의해 깨어났는지 확인하고-ERESTARTSYS
오류 코드를 반환한다. 참 이면 0을 반환합니다condition
.wait_event_interruptible_timeout
매크로는 매크로와 유사 하지만wait_event_timeout
절전 중에 신호에 의해 중단되면ERESTARTSYS
오류 코드를 반환합니다.
5.唤醒等待队列
Linux 커널은 해당 대기열의 프로세스를 깨우기 위해 몇 가지 매크로를 제공합니다. 이러한 매크로의 정의는 다음과 같습니다.
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
wake_up
TASK_INTERRUPTIBLE
매크로는 대기 중인 대기열을 깨우고 대기 중인 프로세스를 깨울 수 있습니다TASK_UNINTERRUPTIBLE
. 이 매크로는wait_event/wait_event_timeout
쌍으로 사용됩니다.wake_up_interruptible
매크로와wake_up()
사이의 유일한 차이점은TASK_INTERRUPTIBLE
프로세스의 상태만 깨울 수 있다는 것입니다.wait_event_interruptible、wait_event_interruptible_timeout
이 매크로는 매크로를 사용하여 절전 모드로 전환된 프로세스를 깨울 수 있습니다 .
동기화 메커니즘 실험
이 섹션에서는 Waiting Queue에 의해 구현된 동기화 메커니즘을 사용하는 실험을 설명하며 독자는 이 섹션의 실험을 통해 Linux의 동기화 메커니즘을 더 깊이 이해할 수 있습니다.
동기화 메커니즘 설계
프로세스 동기화 메커니즘의 설계는 먼저 대기 큐를 필요로 합니다.이벤트가 완료되기를 기다리는 모든 프로세스는 대기 큐에 첨부되며 큐를 포함하는 데이터 구조는 이러한 의도를 실현할 수 있습니다. 이 데이터 구조의 정의 코드는 다음과 같습니다.
struct CustomEvent{
int eventNum; //事件号
wait_queue_head_t *p; //系统等待队列首指针
struct CustomEvent *next; //队列链指针
};
다음은 이 구조에 대한 간략한 설명입니다.
- 라인 2는
eventNum
프로세스가 대기 중인 이벤트 번호를 나타냅니다. - 라인 3은 대기 큐이며 프로세스는 이 대기 큐에서 대기합니다.
- 4행은 이 구조를 연결하기 위한 포인터입니다.
실험의 목적을 달성하기 위해 각각 이벤트 체인 목록의 헤드와 테일을 나타내도록 두 개의 포인터를 설계하였으며, 이 두 구조의 정의는 다음 코드와 같습니다.
CustomEvent *lpevent_head = NULL; //链头指针
CustomEvent *lpevent_end = NULL; //链尾指针
각 이벤트는 연결된 목록으로 구성되며 각 연결 목록에는 이 이벤트를 기다리는 대기 큐가 포함됩니다. 이 구조는 아래 그림에 나와 있습니다.
실험 설계를 구현하기 위해 FindEventNum()
이벤트 목록에서 해당 이벤트에 해당하는 대기자 목록을 찾는 함수를 정의하였으며, 이 함수의 코드는 다음과 같다.
CustomEvent *FindEventNum(int eventNum, CustomEvent **prev)
{
CustomEvent *tmp = lpevent_head;
*prev = NULL;
while(tmp)
{
if(tmp->eventNum == eventNum)
return tmp;
*prev = tmp;
tmp = tmp->next;
}
return NULL;
}
이 기능에 대한 간략한 소개는 다음과 같습니다.
- 라인 1, 함수는 두 개의 매개변수를 받습니다. 첫 번째 매개변수는
eventNum
이벤트의 일련 번호이고 두 번째 매개변수는 반환된 이벤트의 이전 이벤트입니다. 함수가 원하는 이벤트를 찾으면 반환하고 그렇지 않으면 NULL을 반환합니다. - 3행은
tmp
이벤트 목록의 선두로 지정됩니다. - 4줄은
prev
NULL을 가리킵니다. - 5~11행은
while()
원하는 이벤트의 구조 포인터를 찾기 위한 루프입니다. - 라인 7,
tmp
가리키는 이벤트 번호가 와 같은지 판단eventNum
하고, 같으면 반환하여 발견되었음을 표시하고, 그렇지 않으면 연결 목록을 따라 계속 검색합니다. - 10줄,
tmp
뒤로 이동합니다. - 12줄이 없으면 NULL 값을 반환합니다.
실험 설계를 구현하기 위해sys_CustomEvent_open()
새로운 이벤트를 할당하고 새로 할당된 이벤트의 이벤트 번호를 반환하는 시스템 호출 함수를 정의하며 함수의 정의는 다음과 같다.
asmlinkage int sys_CustomEvent_open(int eventNum)
{
CustomEvent *new;
CustomEvent *prev;
if(eventNum)
if(!FindEentNum(eventNum, &prev))
return -1;
else
return eventNum;
else
{
new = (CustomEvent *)kmalloc(sizeof(CustomEvent), GFP_KERNEL);
new->p = (wait_queue_head_t *)kmalloc(sizeof(wait_queue_head_t), GFP_KERNEL);
new->next = NULL;
new->p->task_list.next = &new->p->task_list;
new->p->task_list.prev = &new->p->task_list;
if(!lpevent_head)
{
new->eventNum = 2; //从2开始按偶数递增事件号
lpevent_end->next = lpevent_end = new;
return new->eventNum;
}
else
{
//事件队列不为空,按偶数递增一个事件号
new->eventNum = lpevent_end->eventNum + 2;
lpevent_end->next = new;
lpevent_end = new;
}
return new->eventNum;
}
return 0;
}
이 기능에 대한 간략한 소개는 다음과 같습니다.
- 1줄, 이 함수는 새로운 이벤트를 생성하는 데 사용되며 파라미터는 새로 생성된 이벤트 번호입니다.
- 3행과 4행은 두 이벤트에 대한 포인터를 정의합니다.
- Line 5, 이벤트가 0인지 판단하고, 0이면 이벤트를 다시 생성합니다.
- 6~9행은 이벤트 번호에 따라 이벤트를 검색하여 찾으면 이벤트 번호를 반환하고 없으면 -1을 반환합니다.
FindEventNum()
이 함수는 이벤트 번호에 따라 해당 이벤트를 찾습니다. - 12~31행은 이벤트를 재할당하는 데 사용됩니다.
- 12행,
kmalloc()
이벤트를 할당하는 함수를 호출합니다. - 13행, 이벤트에 해당하는 대기 큐를 할당하고 대기 큐의 작업 구조 링크를 자신에게 가리킵니다.
- 17-22행, 이벤트 체인 목록 헤더가 없으면 새로 할당된 이벤트를 이벤트 체인 목록 헤더에 할당하고 새로 할당된 이벤트 번호를 반환합니다.
- 25-28행, 이벤트 링크 리스트 헤더가 없으면 새로 할당된 이벤트를 링크 리스트에 연결합니다.
- 30행은 새로 할당된 이벤트 번호를 반환합니다.
다음은 이벤트에 대한 프로세스를 차단하는 시스템 호출 함수를 정의하고 대기 중인 이벤트가 깨어날 때까지 이벤트가 종료되지 않습니다. 이 함수의 코드는 다음과 같습니다.
asmlinkage int sys_CustomEvent_wait(int eventNum)
{
CustomEvent *tmp;
CustomEvent *prev = NULL;
if((tmp = FindEventNum(eventNum, &prev)) != NULL)
{
DEFINE_WAIT(wait); //初始化一个wait_queue_head
//当前进程进入阻塞队列
prepare_to_wait(tmp->p, &wait, TASK_INTERRUPTIBLE);
schedule(); //重新调度
finish_wait(tmp->p, &wait); //进程被唤醒从阻塞队列退出
return eventNum;
}
return -1;
}
이 기능에 대한 간략한 소개는 다음과 같습니다.
- 라인 1, 이 함수는 대기열이 대기할 때까지 기다리는 시스템 호출을 구현합니다.
- 3행과 4행은 두 이벤트에 대한 포인터를 정의합니다.
- 5행은
eventNum
이벤트 구조를 찾아 검색에 실패하면 -1을 반환합니다. - 7행은 대기 큐를 정의하고 초기화합니다.
- 8행은 현재 프로세스를 대기 큐에 넣습니다.
- 9행, 새 프로세스 일정을 변경합니다.
- 10행, 프로세스가 깨어날 때 프로세스는 대기 큐에서 나갑니다.
- 라인 11은 이벤트 번호를 반환합니다.
프로세스를 잠들게 하는 기능과 깨우는 기능이 있습니다. 특정 이벤트를 깨우고 대기하는 기능은 이며sys_CustomEvent_signal()
, 이 기능의 코드는 다음과 같습니다.
asmlinkage int sys_CustomEvent_signal(int eventNum)
{
CustomEvent *tmp = NULL;
CustomEvent *prev = NULL;
if(!(tmp = FindEventNum(eventNum, &prev)))
return 0;
wake_up(tmp->p); //唤醒等待事件的进程
return -1;
}
이 기능에 대한 간략한 소개는 다음과 같습니다.
- 라인 1, 함수는 매개변수를 수신합니다. 이 매개변수는 깨어날 이벤트의 이벤트 번호이며 이 이벤트를 기다리는 함수가 깨어날 것입니다.
- 라인 1, 함수는 매개변수를 수신합니다. 이 매개변수는 깨어날 이벤트의 이벤트 번호이며 이 이벤트를 기다리는 함수가 깨어날 것입니다.
- 2행과 3행은 두 개의 구조 포인터를 정의합니다.
- 5줄, 이벤트가 없으면 반환됩니다.
- 7행은 대기 큐의 모든 프로세스를 깨웁니다.
- 8줄, 성공을 나타내는 1을 반환합니다.
이벤트를 닫는 함수가 정의되어 있는데, 먼저 이벤트에 대한 대기 큐를 깨운 다음 이벤트가 차지하는 공간을 비웁니다. 함수의 코드는 다음과 같습니다.
asmlinkage int sys_CustomEvent_close(int eventNum)
{
CustomEvent *prev=NULL;
CustomEvent *releaseItem;
if(releaseItem = FindEventNum(eventNum, &prev))
{
if(releaseItem == lpevent_end)
lpevent_end = prev;
else if(releaseItem == lpevent_head)
lpevent_head = lpevent_head->next;
else
prev->next = releaseNum->next;
sys_CustomEvent_signal(eventNum);
if(releaseNum){
kfree(releaseNum);
}
return releasNum;
}
return 0;
}
이 기능에 대한 간략한 소개는 다음과 같습니다.
- 1줄, 함수는 종료 이벤트를 나타냅니다. 종료에 실패하면 0을 반환하고 그렇지 않으면 종료 이벤트 번호를 반환합니다.
- 3행과 4행은 두 개의 구조 포인터를 정의합니다.
- 5행에서 닫아야 하는 이벤트를 찾습니다.
- 7행, 연결 리스트의 마지막 이벤트라면 이전 이벤트를
lpevent_end
가리킵니다 . - 9행, 연결된 목록의 첫 번째 이벤트인 경우
lpevent_head
두 번째 이벤트를 가리킵니다. - 10행, 이벤트가 중간 이벤트인 경우 중간 이벤트를 제거하고 포인터로 연결합니다.
- 13행, 닫아야 하는 이벤트를 깨웁니다.
- 14행, 이벤트가 차지하는 메모리를 지웁니다.
- 18행은 이벤트 번호를 반환합니다.
실험 검증
위의 코드를 커널에 컴파일하고 새 커널로 시스템을 시작합니다. 그런 다음 시스템에 4개의 새로운 시스템 호출이 있습니다. 4개의 새로운 시스템 호출은 각각 __NR_CustomEvetn_open、__NR_CustomEvent_wait、__NR_CustomEvent_signal和__NR_myevent_close
. 이 4개의 시스템 호출을 사용하여 동기화 메커니즘을 확인하는 프로그램을 작성하십시오.
먼저 이벤트를 열어야 하는데 이 함수를 완성하기 위한 코드는 다음과 같으며, 이 코드는 이벤트 번호가 2인 함수를 보기 위해 열었다가 빠져나간다.
#include <linux/unistd.h>
#include <stdio.h>
#include <stdlib.h>
int CustomEvent_open(int flag){
return syscall(__NR_CustomEvent_open, flag);
}
int main(int argc, char **argv)
{
int i;
if(argc != 2)
return -1;
i = CustomEvent_open(atoi(argv[1]));
printf("%d\n",i);
return 0;
}
이벤트 번호가 2인 함수를 연 후 이 이벤트에서 여러 프로세스가 대기 상태에 놓일 수 있습니다. 프로세스를 대기 상태로 만드는 코드는 다음과 같으며, 다음 코드를 여러 번 실행하고 매개변수 2를 전달하면 프로세스는 이벤트 2의 Waiting Queue에 놓입니다.
#include <linux/unistd.h>
#include <stdio.h>
#include <stdlib.h>
int CustomEvent_wait(int flag){
return syscall(__NR_CustomEvent_wait, flag);
}
int main(int argc, char **argv)
{
int i;
if(argc != 2)
return -1;
i = CustomEvent_wait(atoi(argv[1]));
printf("%d\n",i);
return 0;
}
위의 연산을 수행하면 여러 프로세스가 대기 상태가 되는데 이때 다음 코드를 호출하고 파라미터 2를 전달하여 이벤트 2를 기다리는 여러 프로세스를 깨운다.
#include <linux/unistd.h>
#include <stdio.h>
#include <stdlib.h>
int CustomEvent_wait(int flag){
return syscall(__NR_CustomEvent_signal, flag);
}
int main(int argc, char **argv)
{
int i;
if(argc != 2)
return -1;
i = CustomEvent_signal(atoi(argv[1]));
printf("%d\n",i);
return 0;
}
이벤트가 필요하지 않은 경우 이벤트를 삭제할 수 있으며 이 이벤트를 기다리는 모든 프로세스가 반환되어 실행됩니다.이 기능을 완료하는 코드는 다음과 같습니다.
#include <linux/unistd.h>
#include <stdio.h>
#include <stdlib.h>
int myevent_close(int flag){
return syscall(__NR_CustomEvent_close, flag);
}
int main(int argc, char **argv)
{
int i;
if(argc != 2)
return -1;
i = CustomEvent_close(atoi(argv[1]));
printf("%d\n", i);
return 0;
}
요약
차단 및 비차단은 종종 드라이버에서 사용됩니다. 블로킹을 사용하면 I/O 작업을 일시적으로 수행할 수 없을 때 프로세스가 대기 큐에 들어갈 수 있습니다. I/O 작업을 일시적으로 수행할 수 없는 경우 후자는 즉시 반환됩니다. 이 두 가지 방법은 각각의 장단점이 있으므로 실제 적용 시 선택적으로 사용해야 합니다. 블로킹 및 논블로킹도 대기 큐에 의해 구현되므로 이 장에서는 일부 대기 큐의 사용법도 간략하게 설명합니다.