리눅스 커널 스택에 대해서 궁금합니다.

1.커널 스택은 프로세스마다 하나씩 생성되는 건가요 ?
2.커널 스택에는 어떤 정보가 들어가게 되나요 ? (얼핏 알기로는 해당 프로세스에 인터럽트가 걸리면 이에 필요한 정보들을 커널 스택에 저장해 놓고 사용한다고 하던데요...)
3.프로세스 하나는 4기가의 가상 공간을 사용한다고 들었습니다. 이 중에서 0~3기가 까지는 응용프로그램에서 사용하는 공간이고 3~4기가는 커널이 사용하는 공간이라고 알고 있는데요...
여기서 3~4기가 공간과 커널 스택이 어떠한 연관 관계가 있나요?

고수님들의 답변 부탁드립니다.

----------------------------------------------------------------------------------------------------------------------------------------


1. 커널 스택은 프로세스마다 하나씩 생성됩니다.
일반적인 프로세스는 User Mode 스택과 Kernel Mode 스택을 각각 하나씩 가지고 있습니다.
User Mode에서 Kernel Mode로의 전환은 시스템호출이나 인터럽트가 발생하면 일어납니다.
즉, esp 레지스터는 프로세스가 User Mode이면 User Mode 스택의 top을 가르키다가
Kernel Mode로 전환이 되면 Kernel Mode 스택의 top을 가르킵니다.

2. Kernel Mode 스택은 적어도 다음 두가지 목적으로 사용됩니다. 다른 사용처는 딱히 생각나지 않네요.
가. Kernel Mode로 전환된 프로세스는 언젠간 다시 User Mode로 되돌아가야 합니다.
따라서 User Mode로 전환하기위해 필요한 정보 중 일부를 저장합니다. (일부는 다른 곳에 저장합니다.)
나. Kernel Mode에서 함수를 호출하게 되면 그 함수의 지역변수는 Kernel Mode 스택에서 할당됩니다. (User Mode에서 함수를 호출하면 지역변수가 User Mode 스택에 할당되는 것과 같습니다.)
참고로 80x86 아키텍처에서 커널 스택으로 할당된 공간의 크기는 8KB로 고정되어있습니다. 프로세스 생성때 한번 할당되어 작아지지도 커지지도 않습니다. 따라서 커널에 있는 함수에서는 지역변수를 많이 할당하거나 재귀 함수 호출을 하면 좋지 않습니다.

3. 어떻게 설명드려야할 지 상당히 난해한 질문이군요.
커널은 주소 공간 3~4GB를 사용합니다. Kernel Mode 스택은 Kernel Mode에서만 사용되므로 커널 주소 공간에서 할당됩니다.
즉, User Mode 스택을 가르키는 esp 값을 읽어보면 0GB~3GB 사이의 값을 가지고 Kernel Mode 스택을 가르키는 esp 값을 읽어보면 3~4GB 사이의 값을 가지게 됩니다.

4. 기타

가. 프로세스의 User Address Space는 프로세스마다 각각 다르지만, Kernel Address Space는 모든 프로세스가 동일하게 봅니다. 즉, 프로세스 A와 프로세스 B의 특정 가상주소 (예를 들면 0xa0001000) 은 서로 다른 물리주소로 맵핑될 수 있습니다. 하지만 특정 커널 공간 주소 (예를 들면 0xc0001000) 는 모든 프로세스에서 동일한 물리주소로 맵핑됩니다.
이를 위해서 커널은 kernel master page table (swapper_pg_dir 변수가 가르킴)을 관리하고, 필요할 때마다 특정 엔트리를 kernel master page table에서 해당 프로세스의 page table에 복사를 합니다 -- 이런 작업은 Kernel Mode에서 페이지 폴트가 발생했을 때 페이지 폴트 핸들러가 수행합니다.

나. 시스템호출, 인터럽트 핸들러, 커널 모듈 등은 임의의 프로세스의 context를 이용합니다. 예를 들어, 프로세스 A가 User Mode에서 수행되고 있을 때 timer interrupt가 발생하는 경우 Kernel Mode로 전환하여 timer interrupt service routine을 수행하게 되는데 이때 프로세스 A의 page table과 Kernel Mode 스택을 빌어 쓰게됩니다.
Timer interrupt와 프로세스 A와는 별다른 연관이 없지만 interrupt 발생시점에 프로세스 A가 CPU를 점유하고 있었기 때문에 부하가 큰 context switch 등을 따로 하지 않고 프로세스 A의 context를 그대로 이용하는 것입니다. 다만 커널 모듈 등이 기능의 일부를 따로 커널 쓰레드를 생성하여 구현한 경우라면 그 커널 쓰레드의 context를 사용하겠죠.

출 처 : http://kldp.org/node/73308

커널 2.6.36에서는 file_operations 에서 ioctl 이 제거되었고
대신에 unlocked_ioctl 과 compat_ioctl 이 사용되게 되었다.

unlocked_ioctl 과 compat_ioctl 은 2.6.11 에서 처음 추가되었는데 그 이유는 BKL 이슈에 대한 것때문이다.
BKL 은 Big Kernel Lock 의 약자인데 커널에서 락을 이용하지 않게 하려는 시도가 꾸준히 있어왔다.

그 이유는 커널에서 특히 SMP 구조에서 락에 들어가는 비용이 너무 많기 때문이며, 효율적으로 사용하기 위해서다.
그런 이유로 도입된 것이 RCU(read copy update) 이다.

어쨋든 ioctl 이 호출되면 커널 락이 수행되고 자동적으로 SMP 구조에서는 비효율을 가져오고 있었다.
그래서 사용되는 것이 unlocked_ioctl 이다.

쉽게 얘기해서 모든 CPU에 대해서 lock 을 걸던것을 개별적인 lock 을 걸수 있게끔 바꾼것이다.

간단히 어떻게 해야 하는지 확인하자.

static int extio_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
int param_size;
unsigned int value;

if(_IOC_TYPE(cmd) != IOCTL_FN_ALARM) return -EINVAL;
if(_IOC_NR(cmd) >= ALARM_FN_MAXNR) return -EINVAL;

param_size = _IOC_SIZE(cmd);
if(param_size) {
if(_IOC_DIR(cmd) & _IOC_READ) {
if (unlikely(!access_ok(VERIFY_WRITE, (void *)arg, param_size)))
return -EFAULT;
}
if(_IOC_DIR(cmd) & _IOC_WRITE) {
if (unlikely(!access_ok(VERIFY_READ, (void *)arg, param_size)))
return -EFAULT;
}
}

switch (cmd)
{
caseFN_IOCTL_GET:
{
copy_to_user((unsigned int *)arg, &value, sizeof(unsigned int));
return 0;
}
caseFN_IOCTL_PUT:
{
copy_from_user(&value, (unsigned int *)arg, sizeof(unsigned int));
return 0;
}
}

return -ENOTTY;
}

이것이 전통적인 방식의 ioctl 이다.
이제는 여기서 이렇게 바꾸어야 한다.
static DEFINE_MUTEX(extio_mutex);
static int extio_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
int param_size;
unsigned int value;

if(_IOC_TYPE(cmd) != IOCTL_FN_ALARM) return -EINVAL;
if(_IOC_NR(cmd) >= ALARM_FN_MAXNR) return -EINVAL;

param_size = _IOC_SIZE(cmd);
if(param_size) {
if(_IOC_DIR(cmd) & _IOC_READ) {
if (unlikely(!access_ok(VERIFY_WRITE, (void *)arg, param_size)))
return -EFAULT;
}
if(_IOC_DIR(cmd) & _IOC_WRITE) {
if (unlikely(!access_ok(VERIFY_READ, (void *)arg, param_size)))
return -EFAULT;
}
}

mutex_lock(&extio_mutex)
switch (cmd)
{
caseFN_IOCTL_GET:
{
copy_to_user((unsigned int *)arg, &value, sizeof(unsigned int));
mutex_unlock(&extio_mutex);
return 0;
}
caseFN_IOCTL_PUT:
{
copy_from_user(&value, (unsigned int *)arg, sizeof(unsigned int));
mutex_unlock(&extio_mutex);
return 0;
}
}

mutel_unlock(&extio_lock);
return -ENOTTY;
}
세가지가 추가되었다.
static DEFINE_MUTEX(extio_mutex);
mutex_lock(&extio_mutex);
mutex_unlock(&extio_mutex);

이제 동일한 ioctl 에 접근하는게 아니라면 SMP 구조에서도 mutex 로 보호되는 상태만 아니면 
동시에 ioctl 에 접근할수 있게 되었다.
또 하나 있는게 compat_ioctl 인데 이것은 32bit 와 64bit 간의 호환성을 갖도록 설계된 ioctl 이다.
자세한 것은 다음에 다시 설명하도록 합니다.

출처 :  http://forum.falinux.com/zbxe/?document_srl=553645
printk()

커널 함수에서 사용되는 출력 함수이다.
printf와의 차이는 메시지 기록관리를 위한 로그레벨을 지정할 수 있다는 것이다.

 로그레벨 명령어 의미
"<0>"  KERN_EMERG 시스템이 동작하지 않는다.
 "<1>"  KERN_ALERT 항상 출력
"<2>"  KERN_CRIT 치명적인 정보
 "<3>" KERN_ERR  오류 정보
 "<4>"  KERN_WARNING 경고 정보 
 "<5>"  KERN_NOTICE 정상적인 정보 
 "<6>"  KERN_INFO 시스템 정보 
 "<7>"  KERN_DEBUG  디버깅 정보

위처럼 로그레벨을 지정하는 이유는 kernel source 내에서 원하는 정보만 출력할 수 있게 함이다.

사용법은 다음과 같다

 printk(KERN_ERR"This is KERN_ERR option\n");

다음 명령을 실행해보면 현재의 로그레벨을 확인 할 수 있다.

 $cat /proc/sys/kernel/prink 
      7     4     1     7

[7] : 현재 로그레벨
       이 레벨보다 높은 메시지(숫자가 작은 수)만 출력을 해준다.
[4] : 기본 로그레벨
       printk()함수를 입력하면서 별도로 로그레벨을 입력하지 않을 경우
[1] : 최소 로그레벨
       부여할 수 있는 최소 로그레벨이다.
       이 값이 1이라면 우리가 printk 함수를 입력하면서 0을 부여할 수 없다.
[7] : 부팅시 로그레벨
        부팅시 출력될 레벨을 지정해주는 것이다.

출력되지 않았을 경우 다음과 같은 명령어로 로그버퍼에 기록된 내용을 볼 수 있다. (출력되지 않은 메시지도 볼 수 있음)
dmesg
# cat /proc/kmsg

출 처 : 
http://ok2513.tistory.com/9  

#define spin_lock_init(_lock)
스핀락을 초기화한다

 

#define spin_lock_irqsave(lock, flags)
인터럽트를 disable하고 스핀락을 얻는다

 

void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)
인터럽트를 restore하고 스핀락을 반환한다

출 처 : http://blog.naver.com/snoya00?Redirect=Log&logNo=60106221880 

[출처] [LINUX] spinlock_t|작성자 스노야

container_of(ptr, type, member)

원형은 다음과 같다.
#define offsetof(type, member) ((size_t) &((type *)0)->member)
#define container_of(ptr, type, member) ({	\
		const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
		(type *)( (char *)__mptr - offsetof(type,member) );})

ptr : 멤버의 포인터
type : 이 멤버를 포함하고 있는 컨테이너 스트럭쳐의 타입
member : type structure에 안에서 존재하는 멤버의 이름

결론적으로 얘기하자면 container_of 매크로는 구조체 멤버의 포인터를 이용하여 구조체의 시작 주소를 찾는 역할을 한다.

예제 코드)
#include <stdio.h>

#define offsetof(type, member) ((size_t) &((type *)0)->member)

#define container_of(ptr, type, member) ({      \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetof(type,member) );})

struct type
{
    char ttt;
    int *member;
} con;

int main()
{
    printf("%p\n", &con);
    printf("%p\n", container_of(&(con.member), struct type, member));

    return 0;
}

결과)
0x80496ec
0x80496ec





zimage와 bzImage는 무슨 차이가 있는걸까? 필자는 처음에 z와 b의 의미 때문에 gzip으로 압축하거나 아니면 bzip2로 압축한 것의 차이인줄 알았지만 압축은 gzip으로 같고 단지 z는 압축했단 의미고 b는 'big kernel'이란 뜻인걸 알았다. 왜 이렇게 나눠졌는가?

이미 1.7절에서 언급했던 것 처럼 커널의 크기가 너무 커서 압축 후에도 일정 크기를 넘어가면 zImage 대신 bzImage를 사용해야한다고 했는데 이유는 다음과 같다.

pc가 처음 만들어질 땐 OS로 도스가 사용됐고 이 때 M$의 유명한 분이 640KB면 충분하다고 했단 소릴 들은적이 있을 것이다. 처음 PC가 만들어질 때의 CPU는 8086으로 16bit CPU 였다. 이 프로세서가 지원하는 최대의 메모리는 1MB였기 때문에 모든 어드레스 스페이스가 1MB 내로 제한됐다. 그러므로 램을 640kb 사용하고 나머지 영역엔 MGA, VGA와 같은 다른 디바이스를 할당해 줬다.

문제는 여기서 시작되는데 AT시절의 PC 기본 구조는 현재까지도 계속 유지되고 있기 때문에 PC가 처음 부팅되면 하위 1MB 만을 사용한다고 생각하면 된다. 보호모드라고 알고 있는 386 이상의 cpu가 가진 기능을 사용하지 않고 리얼모드란 8086 호환 모드를 사용하기 때문인데 이는 OS가 보호모드를 사용할 상태를 만들고 전환하기 전까지는 계속 리얼모드로 남아있기 때문이다.

리눅스 커널의 크기가 커서 커널을 읽어들이는 프로그램 크기나 시스템에서 사용되는 약간의 메모리를 제외한 나머지 램의 빈공간에 읽어 들이지 못하면 하위 1MB가 아니라 그 이상의 연속된 메모리에 커널을 읽어 들이고 압축을 푸는 등의 일을 해야할 것이다. 반대로 남은 용량에 커널이 들어갈 수 있다면 당연히 읽어 들이고 압축을 풀면 끝날 것이고...

이렇게 메모리에 처음 적재되고 압축 풀리고 하는 절차와 위치가 다르기 때문에 zImage와 bzImage오 나뉜 것이고 커널 이미지 파일의 앞부분 bootsect와 setup이 각각에 따라 맞는 것으로 합쳐지게된다. 그리고 bzimage의 경우 하위 1M는 사용하지 못하는데 리눅스에선 그렇다!

컴파일 단계에서 make zImage 했을 경우 System is too big. Try using bzImage or modules. 라고 에러가 난다면 더 많은 부분을 module로 만들거나 bzImage를 사용해야한다.


출 처 : http://blog.naver.com/hdalba?Redirect=Log&logNo=130002608246




kernel

태스크 관리자가 구현된 디렉토리. 문맥 교환(context switch)과 같은 하드웨어 종속적인 태스크 관리 부분은 arch/$(ARCH)$/kernel 디렉토리에 구현되어 있다.


arch

리눅스 커널 기능 중 하드웨어 종속적인 부분들이 구현된 디렉토리.


fs

리눅스에서 지원하는 다양한 파일시스템과 open(), read(), write() 등의 시스템 호출이 구현된 디렉토리. 현재 리눅스에는 약 50가지 정도의 파일시스템이 구현되어 있으며 계속 새로운 파일시스템이 개발중이다. 다양한 파일시스템을 일관된 인터페이스로 접근할 수 있도록 하기 위해 리눅스가 도입한 가상 파일시스템(virtual file system)도 이 디렉토리에 존재한다.


mm

메모리 관리자가 구현된 디렉토리.


driver

리눅스에서 지원하는 디바이스 드라이버가 구현된 디렉토리.


net

리눅스 커널 소스 중 상당히 많은 양을 차지하는 이 디렉토리는 리눅스가 지원하는 통신 프로토콜이 구현된 디렉토리다.


ipc

리눅스 커널이 지원하는 프로세스간 통신 기능이 구현된 디렉토리. 이 디렉토리에는 message passing, shared memory, semaphone가 구현되어 있다.
파이프는 fs 디렉토리에, 시그널은 kernel 디렉토리에, 소켓은 net 디렉토리에 구현되어 있다.


init

커널 초기화 부분, 즉 커널의 메인 시작 함수가 구현된 디렉토리. 하드웨어 종속적인 초기화가 arch/$(ARCH)$/kernel 디렉토리 하위에 있는 head.S와 mics.c에서 이뤄지고 나면, 이 디렉토리에 구현되어 있는 start_kernel() 이라는 C함수로 제어가 넘어 온다.


include

리눅스 커널이 사용하는 헤더 파일들이 구현된 디렉토리. 헤더 파일 중에서 하드웨어 독립적인 부분은 include/linux 하위 디렉토리에 정의되어 있으며, 하드웨어 종속적인 부분은 include/asm-$(ARCH) 디렉토리에 정의되어 있다.


others

Documentation - 리눅스 커널 및 명령어들에 대한 자세한 문서 파일들이 존재하는 디렉토리
lib - 커널 라이브러리 함수들이 구현된 디렉토리
scripts - 커널 구성 및 컴파일 시 이용되는 스크립트 들이 존재하는 디렉토리


kernel 2.4 이후 부터는 initrd를 apio로 압축하기 때문에 

kernel 2.6에서 initrd를 mount 하기 위해서는 아래와 같은 순서로 하면 된다.

>> mkdir initrd_mount
>> cd ./initrd_mount
>> cp /boot/initrd-xxx.img initrd.img.gz
>> gzip -d ./initrd.img.gz
>> mkdir mnt
>> cp initrd.img ./mnt/
>> cd ./mnt
>> cpio -idmv < initrd.img

위와 같은 순서로 실행하면 mnt밑에 initrd.img가 압축 해제되있다.

I/O port에 접근하기 위해 필요한 루틴들은 /usr/include/asm/io.h에 모두 포함되어 있으므로 단순히 C프로그램에서 #include 를 포함하기만 하면 된다. 다른 라이브러리는 필요없다. 

그리고 gcc로 컴파일할 때 최적화 옵션(gcc -O1 혹은 그 이상)을 잊지 말아야 한다. 
또한 디버깅을 위해서는 디버거 옵션(gcc -g)을 넣는 것도 중요하다. 

정리하자면 컴파일할 때는 다음의 옵션을 꼭 넣는다. 
% gcc -g -O2 ... 

Linux에서는 어떤 port에 접근하려고 할 때 프로그램에 접근 권한을 주어야 한다. 
이런 기능은 ioperm()이라는 함수에 의해서 이루어진다. 
ioperm()은 unistd.h에 선언되어 있으므로 사용하려면 이를 포함해야 한다. 

문법은 ioperm(from, num, turn_on)으로 from은 접근 권한을 주는 첫 번째 포트번호이고, num은 from으로부터 연속되는 포트의 갯수이다. 

예를 들어, ioperm(0x300, 5, 1)이면 0x300 포트부터 0x304번 포트까지의 접근 권한을 주는 것이다. 
turn_on에 Boolean으로 1을 주면 허가, 0을 주면 접근을 금지 시킨다. 

ioperm이라는 함수는 프로그램이 root의 권한을 가지고 있어야 실행된다. 그리고 ioperm 에서 접근허가 된 포트라도 프로그램이 끝나면 자동적으로 접근금지가 되므로 굳이 ioperm( , ,0)을 줄 필요는 없다. 

ioperm()은 0x000부터 0x3FF 까지의 포트에만 접근할 수 있게 한다. 

더 높은 포트에 접근하려고 하면 iopl()이라는 함수를 사용하여야 한다. 
iopl()은 모든 포트에 대한 접근허가를 단 한번에 준다. 

그러므로 조심하여야 한다. 잘못된 포트로의 접근은 켬퓨터에 치명적인 문제를 일으킬 수 있다. 

포트에 실질적으로 값을 쓰고 읽는 기능을 주는 함수는 inb()/outb()이다. 

원하는 포트(port)에서 한 바이트(8bit)를 읽기 위해서는 inb(port)를, 쓰기 위해서는 outb(value, port)를 사용하는 데 value는 주고자 하는 값이다. 

두 바이트씩 즉 16bit씩 읽고 쓰기 위해서는 inw(port)/outw(value, port)를 사용하는데, 이는 실질적으로 port로 지정한 포트와 그 다음 포트를 합쳐서 읽고 쓰는 것이다. 

또한 32bit씩 읽고자 한다면 inl(port)/outl(value, port)를 사용하며 이는 지정된 port 에서 연속적으로 이어서 4포트를 한꺼번에 읽고 쓰는 것이다. 

포트를  읽고 쓸 때는 근사적으로 1microsecond(백만분의 1초)가 소요된다. 

또한 포트에 접근한 후에 값을 읽고 쓰는 것을 보다 확실히 하기 위해서 약간의 delay시간을 줄 수 있는 데, 이에 쓰이는 함수는 inb_p()/outb_p()와 같이 함수 끝에 _p를 붙이는 것이다. 

이는 pause를 의미하며, delay시간은 io.h에 정의되어 있는 매크로를 사용하여 조종되어질 수 있고, 초기치는 약 4 microsecond 이다. 

이를 바꾸는 방법은 #define REALLY_SLOW_IO (value) 이므로 이를 이용하면 된다. 
그런데 이 pause시간을 정하는 매크로는 0x80포트를 통하여 이루어 지므로 이에 대한 접근권한을 주는 것을 잊지 말아야 한다.

출처 : http://gnudevel.tistory.com/33http://gnudevel.tistory.com/33

+ Recent posts