고급 Linux 드라이버(2) - 장치 드라이버의 차단 및 동기화 메커니즘


머리말

차단 및 비차단은 장치 액세스의 두 가지 기본 방법입니다. 이 두 가지 방법을 사용하여 드라이버는 차단 및 비차단 액세스를 유연하게 지원할 수 있습니다. 차단 및 비차단 드라이버를 작성할 때 대기 대기열이 자주 사용되므로 이 장에서는 대기 대기열에 대해 간략하게 소개합니다.

차단 및 비 차단

阻塞调用이는 호출 결과가 반환되기 전에 현재 스레드가 일시 중단됨을 의미합니다. 함수는 결과가 나올 때까지 반환하지 않습니다. 어떤 사람들은 차단 호출을 동기 호출과 동일시할 수 있지만 실제로는 다릅니다. 동기 호출의 경우 현재 스레드는 많은 경우에 여전히 활성 상태이지만 논리적으로 현재 함수는 반환되지 않습니다.
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_listLinked List에 요소를 추가하거나 삭제할 때 task_lsit커널 내부에서 잠금이 잠기고 수정이 완료되면 잠금이 해제됩니다. 즉, 잠금 스핀 잠금은 task_lsitAND 연산 중에 대기 큐에 대한 배타적 액세스를 실현합니다.
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_upTASK_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줄은 prevNULL을 가리킵니다.
  • 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 작업을 일시적으로 수행할 수 없는 경우 후자는 즉시 반환됩니다. 이 두 가지 방법은 각각의 장단점이 있으므로 실제 적용 시 선택적으로 사용해야 합니다. 블로킹 및 논블로킹도 대기 큐에 의해 구현되므로 이 장에서는 일부 대기 큐의 사용법도 간략하게 설명합니다.

추천

출처blog.csdn.net/m0_56145255/article/details/131653510