열린책들

344(상) + 336(하)쪽

20.09.27 ~ 11.20 (55일)

 

제인 오스틴의 장편 소설. 가장 대표적이고 높은 평가를 받는 작품중 하나이다. 1815년 작품이고, 당시 젠트리 계층(귀족은 아니지만 상류층)인 주인공 엠마의 이야기를 다룬다. 사실 풍속적인 로맨스 소설이고, 이야기의 큰 플로우도 단순하다. 대충 요약하면 다음과 같다.(스포주의)

더보기
1. 주인공 엠마는 다소 지위는 낮지만 친구인 H가 있다.
2. H는 농부인 M의 청혼을 받고 수락할 생각이지만, 엠마가 지위가 너무 낮음을 들어 거부하라고 꼬드긴다.
3. 엠마는 목사인 E가 H와 어울린다고 생각하고, 둘을 이어주려고 한다.
4. 한편, 엠마를 어릴때부터 지켜봐온 N은 엠마의 이런 꼬드김을 비판한다.
5. 하지만 E는 엠마에게 관심이 있었고, 청혼을 하지만 거절당한다. E는 엠마가 H를 거론하자 신분차이 때문에 모욕감을 느낀다.
6. 그 와중에, 멀리 살던 F가 오게 되고, 엠마는 그에게 호감을 느낀다.
7. 또한 J라는 교양있는 여성도 비슷한 시기에 만나게 되는데, 엠마는 그녀를 멀리한다.
8. F에게 호감을 느끼면서도, 엠마는 결혼할 생각이 여전히 없다.
9. 엠마는 H의 이야기를 들으면서, H가 F에게 호감이 있다고 생각한다.
10. 시간이 지나고, 사실 F와 J는 처음부터 비밀 약혼을 한 사이임이 들어난다.
11. 또한 H가 호감이 있던 상대는 F가 아니라 N이였다. 
12. 엠마는 H의 이야기를 들으면서, 자신이 N에게 호감이 있음을 처음으로 자각한다. (갈등의 절정!)\
13. 엠마와 N은 엇갈리지만, 결국 서로의 진심을 확인하게 된다.

요약본만 보면 문학적 가치가 있는 작품이라고 생각하기 힘들다. 사실 나도 아주 옛날에 오만과 편견을 읽고, "제인 오스틴은 재미있는 작품을 쓰지만, 문학적인 가치가 높지는 않구나"라는 생각을 하기도 했다. 그러나 이 작품을 읽으면서 생각이 완전히 바뀌었다. 엠마는 물론 기본적으로 로맨스 소설이지만, 다양하고 입체적인 인물상, 당시 시대상의 뛰어난 묘사, 느리면서도 몰입하게 만드는 전개 등으로 문학적 가치도 훌륭한 소설이다. 게다가 현대의 미디어매체들처럼, "파고들기" 좋은 요소들도 상당히 많다. 무엇보다 소설의 가장 중요한 "재미"라는 점에서 뛰어난 모습을 보여주는 작품이다. 다만 다른 장편 고전 소설들이 그렇듯이, 전개가 다소 느리기 때문에 초반부는 다소 지루할 수는 있다.

 

전체적으로 오만과 편견보다 훨씬 더 뛰어난(오만과 편견도 지금 다시 읽으면 평가가 달라질것 같긴하다.) 작품으로 재미있게 읽었다.

'' 카테고리의 다른 글

스토너 - 존 윌리엄스  (0) 2021.01.07
절망 - 블라디미르 나보코프  (0) 2020.12.27
여학생 - 다자이 오사무  (0) 2020.09.27
창백한 불꽃 - 블라디미르 나보코프  (0) 2020.08.31
사양 - 다자이 오사무  (0) 2020.07.19

[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.

[0]

리눅스 커널에서 모든 유저 프로세스는 가상 메모리 공간을 할당 받는다. 가상 메모리 덕분에, 각 프로세스들은 자신들이 모든 메모리 공간을 '독점'하는 것처럼 사용할 수 있다. 하지만 실제로 물리 메모리를 각 프로세스가 독점할 수 는 없기 때문에, 커널에서는 이런 유저 프로세스의 메모리를 관리할 필요가 있다. 이를 위헤 커널에서는 mm_struct라는 memory descriptor를 각 테스크마다 할당하여 관리한다. 또한 가상 메모리 공간의 다양한 영역(코드 영역, 데이터 영역 등)을 관리하기 위해 vm_area_struct(VMA) 구조체가 영역마다 할당되어 mm_struct에 저장된다. 

 

[1]

우선 각 테스크의 정보를 저장하는 task_struct에서 mm_struct 멤버를 찾아보자.

struct task_struct {
	...
    struct mm_struct	*mm;
    struct mm_struct	*active_mm;
    ...
}

task당 하나의 mm_struct가 mm이라는 멤버로 할당되는 것을 확인할 수 있다. 이때, mm은 모든 테스크마다 unique하지는 않다. 하나의 유저 프로세스에 여러 개의 스레드가 생성된다면, 해당 스레드는 메모리 공간을 공유하므로, mm이 같은 값을 가지게 된다. Task가 스케쥴링되게 되면, mm이 가르키는 mm_struct가 로드 되어 active_mm에 저장된다. 단, 이때 커널 스레드는 mm이 NULL이므로(유저 레벨 메모리 공간이 없으므로) 커널의 메모리 공간이 acive_mm에 로딩된다.

 

[2]

이제 실제 코드로 mm_struct를 분석해보자. 커널버전은 5.7이고, 일부의 멤버만 남겨놨다. 전체 mm_struct는 include/linux/mm_types.h에서 찾을 수 있다.

struct mm_struct {
	struct {
		struct vm_area_struct *mmap;		/* list of VMAs */
		struct rb_root mm_rb;
		
		/**
		 * @mm_users: The number of users including userspace.
		 *
		 * Use mmget()/mmget_not_zero()/mmput() to modify. When this
		 * drops to 0 (i.e. when the task exits and there are no other
		 * temporary reference holders), we also release a reference on
		 * @mm_count (which may then free the &struct mm_struct if
		 * @mm_count also drops to 0).
		 */
		atomic_t mm_users;

		/**
		 * @mm_count: The number of references to &struct mm_struct
		 * (@mm_users count as 1).
		 *
		 * Use mmgrab()/mmdrop() to modify. When this drops to 0, the
		 * &struct mm_struct is freed.
		 */
		atomic_t mm_count;

		unsigned long total_vm;	   /* Total pages mapped */
		unsigned long locked_vm;   /* Pages that have PG_mlocked set */
		atomic64_t    pinned_vm;   /* Refcount permanently increased */
		unsigned long data_vm;	   /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
		unsigned long exec_vm;	   /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
		unsigned long stack_vm;	   /* VM_STACK */
		unsigned long def_flags;

		spinlock_t arg_lock; /* protect the below fields */
		unsigned long start_code, end_code, start_data, end_data;
		unsigned long start_brk, brk, start_stack;
		unsigned long arg_start, arg_end, env_start, env_end;


	} __randomize_layout;

	/*
	 * The mm_cpumask needs to be at the end of mm_struct, because it
	 * is dynamically sized based on nr_cpu_ids.
	 */
	unsigned long cpu_bitmap[];
};
  • mm_users : mm_struct의 주소 공간을 사용중인 task를 나타낸다. 만약 하나의 프로세스에 대해 메모리를 공유하는 스레드가 3개라면, mm_users는 3이 된다.
  • mm_count : mm_struct의 참조 카운트. 0이 되면 mm_struct가 free된다.
  • mmap : 가상 메모리 영역의 리스트. vm_area_struct에 대해서는 아래에서 다룬다.
  • mm_rb : 마찬가지로 가상 메모리 영역을 저장한다. mmap의 원소와 완전히 동일하지만 red-black tree 구조이다. mmap은 VMA를 순회할때 사용되고, mm_rb는 VMA를 검색할 때 사용된다.
  • pgd : 가상메모리 -> 물리메로리 매핑에 사용되는 페이지 테이블의 주소.
  • start_code / end_code : code영역의 시작 주소와 끝 주소. 유사하게, data영역이나(start_data) heap영역(start_brk)도 다른 멤버로 선언되어있다.

[3]

vma_area_struct 역시 mm_struct와 같은 파일에 존재한다. 

/*
 * This struct describes a virtual memory area. There is one of these
 * per VM-area/task. A VM area is any part of the process virtual memory
 * space that has a special rule for the page-fault handlers (ie a shared
 * library, the executable area etc).
 */
struct vm_area_struct {
	/* The first cache line has the info for VMA tree walking. */

	unsigned long vm_start;		/* Our start address within vm_mm. */
	unsigned long vm_end;		/* The first byte after our end address
					   within vm_mm. */

	/* linked list of VM areas per task, sorted by address */
	struct vm_area_struct *vm_next, *vm_prev;

	struct rb_node vm_rb;

	/*
	 * Largest free memory gap in bytes to the left of this VMA.
	 * Either between this VMA and vma->vm_prev, or between one of the
	 * VMAs below us in the VMA rbtree and its ->vm_prev. This helps
	 * get_unmapped_area find a free area of the right size.
	 */
	unsigned long rb_subtree_gap;

	/* Second cache line starts here. */

	struct mm_struct *vm_mm;	/* The address space we belong to. */

	/*
	 * Access permissions of this VMA.
	 * See vmf_insert_mixed_prot() for discussion.
	 */
	pgprot_t vm_page_prot;
	unsigned long vm_flags;		/* Flags, see mm.h. */

	/*
	 * For areas with an address space and backing store,
	 * linkage into the address_space->i_mmap interval tree.
	 */
	struct {
		struct rb_node rb;
		unsigned long rb_subtree_last;
	} shared;

	/*
	 * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
	 * list, after a COW of one of the file pages.	A MAP_SHARED vma
	 * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
	 * or brk vma (with NULL file) can only be in an anon_vma list.
	 */
	struct list_head anon_vma_chain; /* Serialized by mmap_sem &
					  * page_table_lock */
	struct anon_vma *anon_vma;	/* Serialized by page_table_lock */

	/* Function pointers to deal with this struct. */
	const struct vm_operations_struct *vm_ops;

	/* Information about our backing store: */
	unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE
					   units */
	struct file * vm_file;		/* File we map to (can be NULL). */
	void * vm_private_data;		/* was vm_pte (shared mem) */

#ifdef CONFIG_SWAP
	atomic_long_t swap_readahead_info;
#endif
#ifndef CONFIG_MMU
	struct vm_region *vm_region;	/* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
	struct mempolicy *vm_policy;	/* NUMA policy for the VMA */
#endif
	struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

대부분의 멤버들이 주석과 이름을 충분히 활용도를 유추할 수 있다. VMA 하나가, 프로세스의 메모리 공간(code 영역, 데이터 영역 등)을 나타낸다는 점을 상기하면서, 중요한 멤버 몇개만 살펴보자.

 

  • vm_start, vm_end : VMA 영역의 시작 주소와 끝 주소. vm_end - vm_start가 해당 VMA의 사이즈를 나타낸다.
  • vm_prev, vm_next : 다음과 이전 VMA 구조체를 가르키는 포인터. 더블 링크드 리스트 형태로 되어있고, vm_start로 소팅되어 있다.
  • vm_mm : 해당 VMA가 속해있는 mm_struct
  • vm_flags : 해당 메모리 영역의 속성을 나타내는 플래그
  • vm_ops : VMA 영역에 대한 opeartions들을 나타내는 function pointer들
  • vm_file : mmap을 사용한 경우 해당하는 파일, null일 경우 이 VMA는 mmap으로 사용되고 있지 않음.

 

[참고]

esos.hanyang.ac.kr/tc/2015gradproject2/i/entry/2

 

http://esos.hanyang.ac.kr/tc/2015gradproject2/i/entry/2

신건수 컴퓨터 공학부 2010004207 1.    목표 -      리눅스 메모리 관리 기법을 이해한다. -      가상 메모리와 물리 메모리를 이해한다 2.    메모리 관리를 해야하는 이유 태스크를 실행 되기 �

esos.hanyang.ac.kr

Robert Love, Linux Kernel Development 3rd Edition.

+ Recent posts