본문 바로가기
scheduler

Completions - "wait for completion" barrier APIs

by jsh91 2023. 11. 20.

kernel scheduling에 대해 공부해보자

우선 linux kernel 공식 document의 내용부터 정리해보자

 

Completions - "wait for completion" barrier APIs

scheduler를 만들 때 completion은 " locks/semaphores and busy-loops" 보다 선호되는 방법이다

Completions은 waitqueue 와 wakeup 으로 기반으로 구성되어 있다.

Completions 코드는 /kernel/sched/completion.s 에 작성되어 있다.

 

Usage :

complete 사용에 중요한 3가지 부분

1. 'struct completion' 동기화 객체의  초기화

2. ' wait_for_completion()'의 변수 중 하나를 통한 대기

3. ' Complete()'과 ' Complete_all()'를 통해 signal을 전달

 

completions' 기능을 사용하려면 #include <linux/completion.h>를 선언하고 'struct completion'를 할당한다.

struct completion {
        unsigned int done;
        wait_queue_head_t wait;
};

 

wait : 대기 중인 작업을 배치하는 wait queue

done : 완료 flag

 

동기화되는 이벤트를 참조하려면 이름을 아래와 같이 지정한다.

wait_for_completion(&early_console_added);

complete(&early_console_added);

 

 

Initializing completions :

동적으로 할당된 완료 객체는 함수/드라이버의 수명 동안 활성이 보장되는 데이터 구조에 내장되어 비동기식 Complete() 호출이 발생하는 경합을 방지하는 것이 좋습니다.

 

reinit_completion()wait_for_completion()의 _timeout() 또는 _killable()/_interruptible() 변형을 사용할 때는 특별한 주의가 필요합니다. 모든 관련 활동(complete() 또는 )이 수행될 때까지 메모리 할당 해제가 발생하지 않는다는 것을 보장해야 하기 때문입니다. 시간 초과 또는 신호 트리거로 인해 이러한 대기 기능이 조기에 반환되는 경우에도 마찬가지입니다.

 

동적으로 할당된 완료 객체의 초기화는 다음 호출을 통해 수행됩니다 init_completion().

init_completion(&dynamic_object->done);

1. waitqueue를 초기화

2. done -> 0 으로 초기화 ( 완료되지 않음 or 끝나지 않음 )

 

재초기화 기능은 reinit_completion()대기 대기열을 건드리지 않고 단순히 ->done 필드를 0("완료되지 않음")으로 재설정합니다. 이 함수의 호출자는 병렬로 진행되는 까다로운 wait_for_completion() 호출이 없는지 확인해야 합니다.

동일한 완료 개체를 두 번 호출하는 init_completion()것은 대기열을 빈 대기열로 다시 초기화하고 대기열에 추가된 작업이 "손실"될 수 있으므로 버그일 가능성이 높습니다. reinit_completion()이 경우 사용하되 다른 경합에 유의하세요.

정적 선언 및 초기화를 위해 매크로를 사용할 수 있습니다.

파일 범위의 정적(또는 전역) 선언의 경우 다음을 사용할 수 있습니다 DECLARE_COMPLETION().

static DECLARE_COMPLETION(setup_done);
DECLARE_COMPLETION(setup_done);

이 경우 완료는 '완료되지 않음'으로 초기화된 부팅 시간(또는 모듈 로드 시간)이며 호출이 필요하지 않습니다 init_completion().

완성이 함수 내에서 지역 변수로 선언되면 초기화는 DECLARE_COMPLETION_ONSTACK() lockdep을 만족시킬 뿐만 아니라 제한된 범위가 고려되었으며 의도적이라는 점을 분명히 하기 위해 항상 명시적으로 사용해야 합니다.

DECLARE_COMPLETION_ONSTACK(setup_done)

완료 개체를 지역 변수로 사용할 때 함수 스택의 수명이 짧다는 점을 잘 알고 있어야 합니다. 함수는 모든 활동(예: 대기 스레드)이 중지되고 완료 개체가 완전히 완료될 때까지 호출 컨텍스트로 반환되어서는 안 됩니다. 미사용.

다시 한 번 강조하자면, 특히 시간 초과 또는 신호(_timeout(), _killable() 및 _interruptible()) 변형과 같은 더 복잡한 결과가 있는 일부 대기 API 변형을 사용하는 경우 객체가 완료될 수 있는 동안 대기가 조기에 완료될 수 있습니다. 여전히 다른 스레드에서 사용 중입니다. wait_on_completion*() 호출자 함수에서 반환되면 함수 스택 할당이 해제되고 다른 스레드에서 완료()가 수행되면 미묘한 데이터 손상이 발생합니다. 간단한 테스트로는 이러한 종류의 경주가 발생하지 않을 수 있습니다.

확실하지 않은 경우 동적으로 할당된 완료 개체를 사용하십시오. 완료 개체를 사용하는 도우미 스레드의 수명을 초과하는 지루할 정도로 긴 수명을 가진 다른 오래 지속되는 개체에 포함되거나 잠금 또는 기타 동기화 메커니즘이 있어 완료되었는지 확인하는 것이 좋습니다. ()는 해제된 객체에 대해 호출되지 않습니다.

DECLARE_COMPLETION()스택의 순진함은 lockdep 경고를 트리거합니다.

 

Waiting for completions:

스레드가 일부 동시 활동이 완료될 때까지 기다리려면 초기화된 완료 구조에서 wait_for_completion()을 호출합니다

void wait_for_completion(struct completion *done)

일반적인 사용 시나리오는 다음과 같습니다.

CPU#1                                   CPU#2

struct completion setup_done;

init_completion(&setup_done);
initialize_work(...,&setup_done,...);

/* run non-dependent code */            /* do setup */

wait_for_completion(&setup_done);       complete(&setup_done);

이는 wait_for_completion()과 Complete() 호출 사이의 특정 순서를 의미하지 않습니다. wait_for_completion() 호출 이전에 Complete() 호출이 발생한 경우 대기 측은 모든 종속성이 충족됨에 따라 즉시 계속됩니다. 그렇지 않은 경우 완료()가 완료 신호를 보낼 때까지 차단됩니다.

wait_for_completion()은 spin_lock_irq()/spin_unlock_irq()를 호출하므로 인터럽트가 활성화되었음을 알고 있는 경우에만 안전하게 호출할 수 있습니다. IRQ가 꺼진 원자 컨텍스트에서 이를 호출하면 감지하기 어려운 가짜 인터럽트 활성화가 발생합니다.

기본 동작은 시간 초과 없이 기다리고 작업을 중단할 수 없는 것으로 표시하는 것입니다. wait_for_completion() 및 그 변형은 프로세스 컨텍스트(잠자기 가능)에서만 안전하지만 원자 컨텍스트, 인터럽트 컨텍스트, 비활성화된 IRQ 또는 선점이 비활성화된 경우에는 안전하지 않습니다. 원자/인터럽트 컨텍스트에서 완료 처리에 대해서는 아래 try_wait_for_completion()도 참조하세요. .

wait_for_completion()의 모든 변형은 기다리고 있는 활동의 성격에 따라 (분명히) 오랜 시간 동안 차단될 수 있으므로 대부분의 경우 보류된 뮤텍스를 사용하여 이를 호출하고 싶지 않을 것입니다.

 

wait_for_completion*() variants available:

아래 변형은 모두 상태를 반환하며 대부분의 경우 이 상태를 확인해야 합니다. 상태가 의도적으로 확인되지 않은 경우 이를 설명하는 메모를 작성하는 것이 좋습니다(예: arch/arm/kernel/smp.c 참조). :__cpu_up()).

발생하는 일반적인 문제는 반환 유형을 불분명하게 할당하는 것입니다. 따라서 반환 값을 적절한 유형의 변수에 할당하도록 주의하세요.

반환 값의 구체적인 의미를 확인하는 것도 매우 부정확한 것으로 나타났습니다. 예를 들어 다음과 같은 구성이 있습니다.

if (!wait_for_completion_interruptible_timeout(...))

... 성공적인 완료와 중단된 경우에 대해 동일한 코드 경로를 실행합니다. 이는 아마도 원하는 것이 아닐 것입니다.

int wait_for_completion_interruptible(struct completion *done)

이 함수는 대기하는 동안 작업을 TASK_INTERRUPTIBLE로 표시합니다. 기다리는 동안 신호가 수신되면 -ERESTARTSYS가 반환됩니다. 그렇지 않으면 0:

unsigned long wait_for_completion_timeout(struct completion *done, unsigned long timeout)

작업은 TASK_UNINTERRUPTIBLE로 표시되며 최대 '시간 초과' 시간 동안 대기합니다. 시간 초과가 발생하면 0을 반환하고, 그렇지 않으면 남은 시간(최소 1)을 반환합니다.

시간 초과는 코드를 HZ 불변으로 만들기 위해 msecs_to_jiffies()또는 를 사용하여 계산하는 것이 좋습니다 .usecs_to_jiffies()

반환된 시간 초과 값이 의도적으로 무시된 경우 주석에 이유를 설명해야 합니다(예: drivers/mfd/wm8350-core.c wm8350_read_auxadc() 참조).

long wait_for_completion_interruptible_timeout(struct completion *done, unsigned long timeout)

이 함수는 시간 제한을 빠르게 전달하고 작업을 TASK_INTERRUPTIBLE로 표시합니다. 신호가 수신되면 -ERESTARTSYS를 반환합니다. 그렇지 않고 완료 시간이 초과되면 0을 반환하고, 완료되면 jiffies 단위로 남은 시간을 반환합니다.

추가 변형에는 TASK_KILLABLE을 지정된 작업 상태로 사용하고 중단되면 -ERESTARTSYS를 반환하고 완료되면 0을 반환하는 _killable이 포함됩니다. _timeout 변형도 있습니다:

long wait_for_completion_killable(struct completion *done)
long wait_for_completion_killable_timeout(struct completion *done, unsigned long timeout)

_io 변형 wait_for_completion_io()는 대기 시간을 'IO 대기'로 계산하는 것을 제외하고는 _io가 아닌 변형과 동일하게 동작합니다. 이는 작업이 스케줄링/IO 통계에서 계산되는 방식에 영향을 미칩니다.

void wait_for_completion_io(struct completion *done)
unsigned long wait_for_completion_io_timeout(struct completion *done, unsigned long timeout)

 

 

Signaling completions:

계속 조건이 달성되었음을 알리려는 스레드는 완료()를 호출하여 계속할 수 있는 대기자 중 정확히 하나에게 신호를 보냅니다.

void complete(struct completion *done)

... 또는 현재 및 미래의 모든 대기자에게 신호를 보내기 위해 Complete_all()을 호출합니다.

void complete_all(struct completion *done)

스레드가 대기를 시작하기 전에 완료 신호가 전송되더라도 신호는 예상대로 작동합니다. 이는 웨이터가 '구조체 완성'의 완료 필드를 "소비"(감소)함으로써 달성됩니다. 대기 스레드 깨우기 순서는 대기열에 추가된 순서와 동일합니다(FIFO 순서).

Complete()가 여러 번 호출되면 해당 수의 대기자가 계속해서 호출될 수 있습니다. Complete()를 호출할 때마다 done 필드가 증가합니다. 하지만 Complete_all()을 여러 번 호출하는 것은 버그입니다. Complete() 및 Complete_all() 모두 IRQ/원자적 컨텍스트에서 안전하게 호출할 수 있습니다.

언제든지 특정 '구조체 완료'에 대해 Complete() 또는 Complete_all()을 호출하는 스레드는 하나만 있을 수 있습니다. 대기 큐 스핀록을 통해 직렬화됩니다. Complete() 또는 Complete_all()에 대한 동시 호출은 아마도 설계 버그일 것입니다.

IRQ 컨텍스트에서 완료 신호를 보내는 것은 spin_lock_irqsave()/spin_unlock_irqrestore()로 적절하게 잠기고 절대 잠들지 않으므로 괜찮습니다.

 

try_wait_for_completion()/completion_done():

The try_wait_for_completion() function will not put the thread on the wait queue but rather returns false if it would need to enqueue (block) the thread, else it consumes one posted completion and returns true:

bool try_wait_for_completion(struct completion *done)

Finally, to check the state of a completion without changing it in any way, call completion_done(), which returns false if there are no posted completions that were not yet consumed by waiters (implying that there are waiters) and true otherwise:

bool completion_done(struct completion *done)

Both try_wait_for_completion() and completion_done() are safe to be called in IRQ or atomic context.

 

 

 

참고 문헌

https://docs.kernel.org/scheduler/completion.html

https://blog.dasomoli.org/248/

 

 

'scheduler' 카테고리의 다른 글

Capacity Aware Scheduling  (2) 2023.11.26
Scheduler Domains  (1) 2023.11.23
CFS Scheduler  (1) 2023.11.22
CFS Bandwidth Control  (1) 2023.11.22
CPU Scheduler implementation hints for architecture specific code  (1) 2023.11.22

댓글