[0]

timer interrupt는 하드웨어에 의해 주기적으로 발생하고, 이에 해당하는 handler가 실행된다. 이는 커널에서 가장 중요한 모듈 중 하나이다. 이 포스팅에서는 timer interrupt hander의 역할에 대해 알아보고, 간단히 코드를 분석한다.

 

[1]

코드를 보기전에 timer interrupt가 왜 필요한지 생각해보자. 운영 체제의 입장에서 시간이라는 개념이 왜 필요할까? 유저에게 시간을 알려주기 위해서? 시간이야 서버에서 받아올 수도 있다.(물론 이 경우 서버의 운영체제는 시간을 어떻게 계산하냐는 문제가 생기겠지만) 뭐 매 초마다 네트워크로 받아오는 건 오버헤드가 너무 클 테니, 1분마다 받아오는 정도로 타협하면 충분할 거다. 그런데 "1분마다 서버에서 현재 시각을 받아온다"의 "1분마다"는 어떻게 운영 체제에서 알 수 있을까? 이를 위해서 운영 체제에서 관리하는 시간이 필요할 수밖에 없다. 커널에서는 하드웨어에 의해 주기적으로 발생하는 timer interrupt를 이용하여 시간을 관리한다. 또한 이를 이용하여 1분마다 특정한 함수를 호출하는 식의 동작도 가능해진다.

 

다른 관점에서 하나 더 필요성을 생각해보자. 커널 task와 유저 task들은 모두 스케줄러에 의해서 실행된다. 그런데 preept 스케줄러의 경우, task가 완료될때까지 실행되게 두지 않고, 적당한 시점에 preept하여 다른 task를 실행시키게 된다. 그런데 task들이 yeild하지 않는다면 스케줄러 입장에서는 preept가 불가능하게 된다. 커널 task야 곳곳에 스케줄링 포인트를 삽입할 수 있겠지만, 유저 task에 이를 강요하는 것은 불가능하다. 애당초 커널은 유저 코드를 믿지 않고 동작해야 하기 때문이다(유저 코드의 잘못이 커널에까지 전파되면 안 되므로). 따라서 별도의 스케줄링 포인트가 필요한데, 이는 timer interrupt에 스케줄링 포인트를 넣으므로서 해결할 수 있다. timer interrupt는 하드웨어에 의해 주기적으로 발생하므로, 현재 실행 중인 task의 코드에 관계없이 interrupt handler가 실행될 수 있고, 여기에 스케줄링 포인트를 넣어서 스케줄링 컨텍스트로 넘어갈 수 있다.

 

[2]

timer interrupt handler는 아키텍처 dependent한 부분을 재빠르게 처리하고, tick_periodic에서 아키텍처 independent한 부분을 처리한다. 여기서는 tick_periodic()의 코드를 보자. 커널 버전은 5.7이다.

/*
 * Periodic tick
 */
static void tick_periodic(int cpu)
{
	if (tick_do_timer_cpu == cpu) {
		raw_spin_lock(&jiffies_lock);
		write_seqcount_begin(&jiffies_seq);

		/* Keep track of the next tick event */
		tick_next_period = ktime_add(tick_next_period, tick_period);

		do_timer(1);
		write_seqcount_end(&jiffies_seq);
		raw_spin_unlock(&jiffies_lock);
		update_wall_time();
	}

	update_process_times(user_mode(get_irq_regs()));
	profile_tick(CPU_PROFILING);
}

우선 jiffies(커널에서 tick마다 증가시키는 카운터)에 대한 lock을 가져온다. do_timer()에서 다음과 같이 jiffies를 증가시킨다.

/*
 * Must hold jiffies_lock
 */
void do_timer(unsigned long ticks)
{
	jiffies_64 += ticks;
	calc_global_load(ticks);
}

update_wall_time()에서는 wall time이라고 하는 실제로 소요된 시간(cpu time과는 다르다) 업데이트 한다.

 

update_process_times()는 현재 실행 중인 프로세스의 run time을 1tick 증가시킨다. 

/*
 * Called from the timer interrupt handler to charge one tick to the current
 * process.  user_tick is 1 if the tick is user time, 0 for system.
 */
void update_process_times(int user_tick)
{
	struct task_struct *p = current;

	/* Note: this timer irq context must be accounted for as well. */
	account_process_tick(p, user_tick);
	run_local_timers();
	rcu_sched_clock_irq(user_tick);
#ifdef CONFIG_IRQ_WORK
	if (in_irq())
		irq_work_tick();
#endif
	scheduler_tick();
	if (IS_ENABLED(CONFIG_POSIX_TIMERS))
		run_posix_cpu_timers();
}

또한 update_process_time은 scheduler_tick()을 호출하는데, 여기에서는 실행중인 프로세스가 할당받은 시간을 다 소진하였는지 체크한다. 이때 눈여겨볼 점은 '현재 실행중인 프로세스'만 체크한다는 점이다. 즉, 하나의 tick내에서 프로세스가 여러 번 switch되었더라도, 마지막(=timer interrupt가 발생한 시점)에 실행된 프로세스가 tick 전체를 사용한 것으로 간주한다. 불공평하지만 tick내의 switch된 프로세스를 모두 추적하기에는 비용이 너무 크기 때문에 이러한 방식을 사용한다. tick이 충분히 자주 발생한다면 이러한 불공평함은 감당할 수 있다.

 

 

 

참고

medium.com/pocs/%EB%A6%AC%EB%88%85%EC%8A%A4-%EC%BB%A4%EB%84%90-%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EA%B0%95%EC%9D%98%EB%85%B8%ED%8A%B8-6-9f5be9df434e

 

리눅스 커널(운영체제) 강의노트[6]

지난 5강에선 Timeslice 라는 CPU에게 주어지는 사용시간과 CPU의 사용 순서를 관리하는 커널 스케쥴링에 대하여 공부를 했다. 이번 시간에는 그 시간의 단위에 대한 공부와 여러 개의 인터럽트가 일

medium.com

Robert Love, Linux Kernel Development 3rd Edition.

+ Recent posts