Linux/UNIX2012. 12. 16. 00:05

오늘은 커널이 무엇인지 쉘이 무엇인지 알아보도록 하겠습니다.

먼저 커널과 쉘이 도대체 어디에 있는 녀석인지 쉽게 알기 위해서 리눅스의 시스템 구조도를 살펴보도록 하자.




제일 하단부에는 우리가 눈으로 직접 볼 수 있는 하드웨어들이 있고 그 위에 커널이 있다. 또 그 위에는 쉘이 있고 유저는 쉘 상에서 명령어를 통한 하드웨어 조작을 할 수 있다. 하드웨어는 여러분들도 잘 알고 있을 테니 설명을 생략하도록 합니다. 그럼 먼저 커널이 무엇인지 간략하게 살펴보자.


<커널>

- 사전적 정의에 의하면 커널은 컴퓨터 운영체제의 가장 중요한 핵심이다.

- 운영체제의 다른 모든 부분에 여러 가지 기본적인 서비스를 제공하기 때문에 윈도우 그 자체라고 보면 된다.

- 커널은 리눅스가 처음 부팅될 때 메모리로 로딩 된다.

- 쉽게 생각하면 여러분들의 윈도우를 부팅할 때 나오는 로딩화면이 바로 커널을 로딩 하는 것이다.

- 컴퓨터의 시스템 자원들을 관리한다.

- 항상 메인 메모리에 상주하기 때문에 윈도우 부팅 후 바로 작업관리자를 열어 메모리 사용량을 보면 0%가 아닌 것이다.

- 리눅스의 커널은 1만줄 이상의 C언어와 1000줄 정도의 어셈블리어로 구성되어 있어서 이식성(portability)이 좋다. 즉 다른 시스템 환경에서도 잘 적응할 수 있다는 것이다.


요약하면 커널은 주로 하드웨어 자원(하드디스크, 메모리 등..)을 효율적으로 관리하기 위해서 필요한 요소라고 생각하면 된다. 커널이 시스템 자원을 관리한다고 말했지만 관리가 일어나기 위해서는 어떤 명령이 실행된다던지 프로그램이 실행되어야 한다. 그리고 보통 그런 명령의 실행은 유저의 입력에 의한 것이 대부분이다. 이 때 유저의 입력을 어떻게 받아서 처리 할 것인지를 결정하고 도와주는 녀석이 바로 쉘이다.


<>

- 운영체제 상에서 다양한 운영 체제 기능과 서비스를 구현하는 인터페이스를 제공하는 프로그램이다.

- 리눅스 명령어를 해석하는 명령어 해석기로 사용자와 리눅스 OS간의 인터페이스와 Shell Programming언어를 해석한다.

- 키보드와 같은 단말 장치를 통해서 유저의 입력을 받아서 여러 프로그램이나 명령을 실행한다.

- sh(본 쉘), ash, bash, csh(C )같은 다양한 쉘이 존재한다.

- 윈도우에서 지원하는 cmd창도 쉘의 일종(CLI)이라고 보면 된다.

- 내가 임의로 JHShell을 만들어서 ls 명령어를 JHls로 만들고 싶다면 그 쉘이 기존의 ls명령어를 수행하게끔 프로그래밍이 되어야한다. 지금 존재하는 쉘 또한 이런 방식으로 만들어졌다고 이해하면 조금 쉬울 것이다.

- 이렇게 JHls를 만들어 놓은 것이 하나의 인터페이스가 된다.


이제다시 위의 그림을 봅시다. 유저는 쉘과 대화를 나누고 다시 쉘은 커널과 대화를 나누는 식이라는 것을 그림으로 표현해놓은 것이다. 그래서 쉘과 커널사이에 인터페이스가 있는 것이다. 쉘을 사용하면 유저는 복잡한 커널단위의 연산들을 알 필요 없이 쉘 상의 인터페이스로 시스템 자원들을 쉽게 관리할 수 있다. 이것이 바로 쉘이 생겨난 이유이다. 추가적으로 윈도우는 그래픽 쉘로서 GUI(그래픽 유저 인터페이스)를 제공한다. 때문에 일반 사용자들이 복잡한 cd명령어를 모르고도 단지 마우스 클릭만으로 디렉터리 안으로 접근할 수 있는 것이다.




Posted by twinjh
Linux/UNIX2012. 11. 23. 15:44

C언어를 조금 공부했거나 C언어 프로그래밍을 조금 해본 사람들은 다음과 같은 메인함수 형태를 본 적이 있을 것이다.


int main(int argc, char *argv[]){

...

}

밑줄친 부분이 바로 오늘 살피고자 하는 부분이다.

간단하게 말하면 agrc는 메인 함수로 전달받은 인자의 개수이고, argv는 전달받는 인자 즉 문자열이라고 보면 된다.


리눅스 환경은 CLI(Command line interface)로서 쉘상에서 유저가 입력한 명령어를 한 줄씩 수행하는 환경이다. 우리는 리눅스 환경에서 gcc(혹은 cc)명령어로 .c 파일을 컴파일 하고, a.out혹은 사용자가 지정한 이름의 실행 파일 이름을 입력시켜서 프로그램을 시작한다.


설명의 용이함을 위해서 예를 들어보자


gcc -o hello hello.c


위와 같은 명령어를 입력하면 hello.c가 hello 라는 이름으로 컴파일되서 그 이름으로 프로그램을 실행 시킬수 있다. (우리가 알고 있는 .exe 와 같은 파일을 생성한 것이라고 보면 된다.) 앞에서 메인 함수로 인자를 전달한다고 했는데 프로그램을 실행 시키면서 같이 명령어를 써주는 것이 인자가 되는 것이다.


예 -> hello my name is J(실행파일 + 인자)

실행 파일 이후로 빈칸이 기준이되어서 각각의 인자로 인식한다. 즉 argv[argc] => argv[4]가 되고 arg[0]은 hello가 되는 것이다.



*예제

#include <stdio.h> // echoarg.c

int main(int argc, char *argv[]) {

int i;

for (i = 0; i < argc; i++) /* echo all command-line args */

printf("argv[%d]: %s\n", i, argv[i]);

exit(0);

}


각각의 argv에 명령어 한 줄에 입력한 인자를 순서대로 보여주는것을 확일할 수 있다.




Posted by twinjh
Linux/UNIX2012. 11. 21. 10:52

모든 프로세스는 자신의 부모로 반환하는 어떤 수인 exit 코드가 존재하고, 시작된 프로세스는 종료되기 마련이다. 모든 프로세스는 종료된다는 말이다. 일반적인 프로세스의 종료방법에는 두 가지가 있는데, 하나는 exit function호출을 통한 종료이고 다른 하나는 그 프로그램의 메인 함수의 리턴을 통한 종료이다. 추가로 프로세스는 어떤 시그널(abort()함수를 통한...)에 대한 응답으로써 비정상적으로 종료되기도 하는데 이곳에서는 정상적인 프로세스에 대해서만 다루기로 한다.


리눅스에서 exit가 들어간 함수는 보통 프로세스의 종료와 관련되어 있다.

exit()이라는 함수 하나만 존재한다면 이런 글을 쓰고 있지 않겠지만, 프로세스를 종료시키는 라이브러리 함수가 몇 가지 더 존재한다.


exit()    _exit()    atexit()

위의 함수는 함수 명에서 보듯 프로세스를 종료시키는 함수이지만, 약간의 차이점을 두고 프로세스를 종료시킨다.

이제 프로세스를 종료시키는 세 가지 방법을 살펴보도록 하자.


#include <stdlib.h>


void exit(int status);

모든 표준 입/출력 버퍼를 비운다. 즉, Cleanup Processing을 한다고 보면되는데, 파일과 같은 모든 스트림들을 닫고 출력버퍼의 내용을 디스크에 쓰고난 뒤에 커널(kernel)로 리턴되는 함수이다. int status는 프로세스의 리턴값(즉, 메인함수의 반환값)이라고 이해하면 되겠다.


#include <unistd.h>


void _exit(int status);

위에서 말한 Cleanup Processing을 수행하지 않고 바로 커널로 리턴한다. Cleanup Processing을 거치지 않기 때문에 지금 수행중인 작업과 이후의 작업에 대해서는 보장받지 못한다는 점이 있다.


위의 두 함수는 모두 프로세스를 정상적으로 종료시킨다.


#include <stdlib.h>


void atexit(void (*func)(void));


returns: 0 if OK, nonzero on error


exit handler를 등록하는 함수이다. 그러니까 프로그램이 종료될 때(exit()이 호출되었을 때) 수행하는 함수들을 등록하는 함수라고 보면 되는 것 이다.




Posted by twinjh
Linux/UNIX2012. 11. 10. 00:51

컴퓨터에서 시그널이란 프로세스가 예측하지 못한 이벤트로부터 발생한 인터럽트를 말하는 것이다. 

인터럽트의 종류는 대략 다음과 같다.

1. 불법적인 연산 (=0으로 어떤 수를 나누기)

2. 컴퓨터 파워의 고장

3. 알람

4. 자식 프로세스의 종료

5. 유저의 입력에 의한 종료와 중단 (Ctrl + C, Ctrl + Z)


signal() 함수는 위에서 명시한 시그널들의 처리를 어떻게 할 것인가를 설정하는 함수이다.

즉, 어떤 시그널이 발생하면 직접 처리를 할것인지 기존에 사용하던 방법을 따를것인지 혹은 무시를 할것인지를 signal()함수를 통해서 설정할 수 있다는 것이다.


헤더

 #include<signal.h>

형태

 void (*signal(int signum, void (*handler)(int)))(int);

인수 

 int signum              시그널 번호

 void (*handler)(int) 시그널을 처리할 핸들러

반환 

 void *()(int);이전에 설정된 시그널 핸들러


예제


#include<stdio.h> // timelimit.c

#include<signal.h>


int delay;

void childHandler( );


int main(int argc, char *argv[]){

int pid;


sscanf(argv[1], "%d", &delay); //delay = strrol(argv[1], 0, 0);

signal(SIGCHLD, childHandler);

pid = fork();        //ceate child

if(pid == 0){ //child

execvp(argv[2], &argv[2]);

perror("Limit");

}else{ //parent

sleep(delay);

printf("Child %d exceeded limit and is being killed\n", pid);

kill(pid, SIGINT);

}

}


void childHandler( ){ /* Executed if the child dies before the parent */

int childPid, childStatus;

childPid = wait(&childStatus);

printf("Child %d terminated within %d seconds\n", childPid, delay);

exit(0);

}


결과





1. 예제에 대한 간략한 설명을 하자면, 메인함수가 시작되면서 프로세스 실행시 사용자가 입력시켜 주었던 argv[1]인자(실행 화면에서 입력한 5)는 sscanf로 전역변수 delay에 할당된다. 


2. 이어서 예제의 중간부에 나온 fork()를 통해서 자식프로세스가 생성되는 것을 볼 수있다. 자식 프로세스는 사용자가 입력해 주었던 두번째 값을 실행시킨다. 이 때 부모 프로세스는 초기에 입력시켜 주었던 5 동안 sleep명령어를 진행하고 있다. 


3. 자식 프로세스의 작업이 끝나면 자식 종료에 대한 시그널이 발생하며 코드의 11번째 라인에있는 시그널 처리함수에 걸려서 핸들러함수로 이동하여 시그널에 대한 처리를 진행하게 된다. 


4. 만약 자식 프로세스가 수행하는 명령어가 5초 이상의 시간을 필요로 하는 작업이라면, 부모가 그동안 진행하고 있던 5초 간의 sleep 함수가 종료되면서 부모가 먼저 자식 프로세스를 kill하게 된다.


이 과정 중 집중해서 봐야할 부분은 3번이다.


signal(SIGCHLD, childHandler);

함수의 첫 번째 인자인 SIGCHLD는 자식 프로세스가 종료될 때 부모에게 전달되는 시그널이다. 즉, 자식 종료 이벤트를 캐치할 것이라고 명시한것으로 보면 된다. 두 번째 인자는 자식이 종료되는 시그널이 발생한다면 사용자가 작성한 childHandler함수로 가서 시그널을 처리할 것임을 알려주는 것이다.

이 글에서는 설명하지 않았지만 #include<signal.h>에는 31가지 이벤트에 대한 시그널 집합 변수가 있는데 참조하고 싶은 분은 아래의 링크로 가서 나오는 글의 테이블을 보시면된다. 

Posted by twinjh
Linux/UNIX2012. 11. 7. 16:13

http://forum.falinux.com/zbxe/?mid=C_LIB&page=9&document_srl=408138

Posted by twinjh
Linux/UNIX2012. 11. 7. 01:11

리눅스는 동시에 멀티 유저 / 멀티 태스킹의 실행을 지원할 수 있는 것을 특징으로 하는 대화형 운영 체제라는 특징이 있다. 리눅스가 다중 사용자를 지원하는 운영체제라는 점을 주의 깊게 살펴야 하는데, 이런 리눅스의 특징 때문에 파일에 대한 권한 이라는 문제가 생기게 되었다.


사용자는 각자 자신의 계정(ID)과 암호를 가지고 로그인을 하여 리눅스환경에 접속하고, 다른 사용자가 자신의 삭제하거나 수정할 수 없도록 파일에 대한 권한을 설정 할 수 있다. (파일 권한은 다들 아시겠지만 잘 모르시는 분들을위해 더보기에 간략한 설명을 해보았다...)



아무튼간에 파일에 대한 권한이 없다면 다른 사용자는 내 파일에 접근할 수가 없게된다. 즉, 해당 디렉토리나 파일에 대한 읽기, 쓰기, 실행권한을 가지고 있어야 다른 사용자가 내 파일이나 디렉토리에 접근하여 파일을 생성하거나 읽을 수 있는 것이다.


rwx 외에도 권한에는 다음 세가지의 추가적인 속성이 있다.


- 04000 : set-user-id(set UID)

02000 : set-group-id(set GID)

01000 : save-text-image(sticky bit)


실행 파일에 setuid가 지정되어 있다면, 파일에 포함된 프로그램이 수행되기 시작할 때 UNIX는 생성되는 프로세스에게 파일 소유주의 UID를 effective uid로 부여한다. 다시 말하면, 프로세스는 프로세스를 생성시킨 사용자의 UID(real UID)를 가지는 것이 아니라 파일 소유주의 UID(effective UID)를 가지는 것이다.


어떤 파일의 소유자(A)가 그 파일에 set UID 비트를 설정해 놓았다면, 다른 사용자(B)가 그 파일을 실행 시켰을 때 그 프로세스의 Effective User ID는파일의 소유자의 UID(User ID)인 A가 된다. 마찬가지로 set GID 비트가 설정 되었다면 파일 실행시 해당 프로세스는 Group ID의 권한으로 실행되는 것이다.


* 참고

Effective User ID(euid) : 실행된 프로세스의 UID

Real User ID(uid) : 실제로 그 프로세스를 실행시키는 유저의 UID


이해를 위해서 한가지 예제를 준비했다.


- /bin/passwd 파일을 자신의 현재 디렉토리에 복사해 온다. =>  $ cp /bin/passwd

다음과 같이 자신의 현재 디렉토리에 있는 passwd 명령을 수행한다. =>  $ ./passwd


위의 명령어를 잘 수행했다면 여러분은 실패 메세지를 보게 되었을 것이다.


- $ passwd & 를 입력

- $ ./passwd & 를 입력

- $ ps -ef | grep passwd 를 입력


결과 화면을 보면, 가장 좌측에 나오는 것이 UID이다. passwd 와 ./passwd 의 UID가 다른것이 보일것이다. 이런 결과가 나오는 이유는 다음과 같다.


리눅스에 등록되어 있는 사용자의 계정 정보는 /etc/passwd 속에 정의되어 있다그 안에는 각 사용자계정의 암호가 특정한 형태로 저장되어 있으며, 사용자는 passwd명령어를 사용해서 자신의 암호를 바꾼다. 이 때 passwd파일에는 여러 사용자의 계정정보가 저장되어 있기 때문에 접근을 통제하지 않으면 보안상 문제가 될 수 있다. 그렇기 때문에 passwd파일은 root만이 접근 가능하도록 되어 있다. 여기서 root만이 접근 가능한 passwd를 일반 사용자가 접근하여 자신의 암호를 변경할 수 있는 이유는, 그 파일에 setuid가 지정 되어 있기 때문이다. setuid가 지정되어 있기 때문에 일반 사용자가 passwd를 실행하여 생성되는 프로세스의 Effective User ID는 파일 소유주인 root의 UID로 부여된다. 그래서 우리가 본래 root만이 접근할 수 있던 passwd에 접근하여 암호를 변경할 수 있던 것이다. 하지만 이 passwd파일을 복사하면 소유자가 자기 자신으로 설정되기 때문에 euid는 자신의 UID가 되고 권한이 없기 때문에 실행하여도 암호를 변경할 수 없게 되는 것이다.





Posted by twinjh