컴퓨터 구조 요약

- CPU - RAM, HDD/SSD, IO 장치

 

멀티 태스크와 가상 CPU

- 한 순간에 CPU는 하나의 프로세서만 처리 가능. 시간을 잘게 나누어 여러 프로세스를 돌아가며 처리하여 다중 작업 수행

- 가상 CPU : 멀티 태스크 작업을 위해 하나의 CPU를 시간을 나눠 스케줄링을 통해 여러 작업을 동시 수행하는 것

 

가상 메모리 : 부족한 1차 메모리 공간을 보완하고, 프로세서로부터 OS를 보호하기 위한 방법.

- 각 프로세서는 2^32 = 4gb 정도의 가상 메모리 공간을 가짐, 4kb 고정 크기 페이지로 나뉘어짐

- 각 가상 메모리는 코드 영역(프로그램), 데이터 영역(정적변수), 힙(동적할당), 스택(매개변수, 지역변수) 영역으로  구분  

- 프로세서는 메모리 관리 유닛 MMU/운영체제에 의해 가상 메모리 주소가 1차+ 2차 메모리 물리 메모리 주소다 맵핑

   -> 실제 물리주소는 OS가 관리하여 프로세서가 타 프로세서의 공간을 읽고쓰기는 것을 방지 => OS 보호

- 각 프로세서는 큰 가상 메모리 공간을 가져도 실제 사용하는 곳만 1차 메모리다가 할당. 쓰지 않는 건 2차 메모리서 보관

- page in(swap in), page out(swap out) : 2차 메모리에 있는 걸 1차메모리로 옮겨와 사용(page in), 반대의 경우 (page out)

- IPC 프로세서간 통신 : OS가 두 ps간 한 가상 주소를 같은 물리 주소로 맵핑시 ps끼리 메모리 공유 = 서로RW가능 = 통신

- memory mapped file : 파일을 메모리에 맵핑 -> 메모리 RW = 파일 RW (시스템 콜 : mmap)

 *nand2tetris 에선 memory mapped io , io장치 입출력을 메모리에 맵핑하고 그 매모리 값을 읽고쓰면서 제어함.

 

메모리 API

#include <stdlib.h>

- void *malloc(size_t size); : size만큼 동적 메모리 공간(힙) 할당. 시작 주소 void *로 반환

- void *calloc(size_t nmemb, size_t size); : nmemb x size 를 힙에 할당, malloc과 차이는 초기값 0 설정

- void *realloc(void *ptr, size_t size) : malloc or calloc으로 할당된 공간을 축소하거나 늘림.

- void free(void *ptr); : 동적할당 해제

 

빌드 과정

- 전처리 : 헤더 파일 인클루드, 매크로 처리 

- 컴파일 : 소스 코드 -> 어셈블리 파일

- 어셈블 : 어셈블리 파일 -> 오브젝트 바이너리(ELF, a.out <- aseember output)

- 링킹 : 오브젝트 바이너리와 필요한 라이브러리 연결하여 최종 실행파일 or 라이브러리(*.a or *.so) 만듬

 

라이브러리

- 정적 라이브러리 : 컴파일시 포함, *.a(리눅스)/*.lib(윈도우) 파일

- 공유/ 동적 라이브러리 : 빌드 시 체크, 링크 로더가 런타임 시작 시 포함. *.so(리눅스), *.dll(윈도우)

 -> 보통 동적 링크 사용

- file 명령어 : 어떤 플랫폼 실행파일인지, 어떻게 링크 됬는지 확인 가능

- ldd 명령어 : 어떤 lib가 링크되고 링크 로더가 뭔지확인, 아래의 경우 libc.so.6가 링크됨. 링크 로더는 /ld-linux-x86-64.so.2

 

프로세스 생성, 명령어 호출, 대기 API

#include <unistd.h>

- pid_t fork(void) : 자식 프로세스 생성. 부모는 자식의 pid 반환, 자식은 0반환. 이후 자식은 exec로 타 작업 수행

- int exec(const char *path) : path 명령어 수행, 반환이 없으나 실패시 -1, errno에 에러코드 등록

  1. 변형 :  execl, execlp, execle 등 존재

  2. l은 실행 인자를 가변으로 전달(마지막엔 NULL), v는 벡터 실행 인자 전달

  3.  e는 마지막에 환경 변수 추가, p는 프로그램을 환경변수 PATH보고 탐색(없으면 절대/상대경로로 찾음)

  4.  ex) execl("/bin/cat", "hello.c", "ls.c", NULL);

#include <sys/types.h>

#include <sys/wait.h>

- pid_t wait(int *status) : 자식 프로세스 중 아무거나가 끝날때까지 기다림. status에는 exit()인자인 종료코드와 종료방법 플래그합친거

- pid_t waitpid(pid_t pid, int *status, int options) : 지정한 pid의 자식이 종료될때까지 기다림. 

 -> 아래는 종료 방법 매크로 

  1. WIFEXITED(status) wait if exited : exit로 종료한 경우 0 아님 -> exit로 종료시 0이 아닌 값이 반환

  2. WEXITSTATUS(status) wait exit status : exit 종료시 종료코드 반환

  3. WIFSIGNALED(status) wait if signaled : 시그널로 종료시 0 아님

  4. WTERMSIG(status) wait term signal?: 시그널 종료 시 시그널 번호 반환

 

프로세스 활용 예제

- spawn.c : 첫째 인자는 실행 프로그램 경로, 둘째 인자는 전달할 인자로 해당 프로그램 실행하는 예제

 

 

프로세스 사이클

#include <stdlib.h>

- void exit(int status) : status는 종료코드(리눅스서 0 성공, 1에러,딴데선 다를수있음), stdio 버퍼 해제(_exit()는 시스템콜)

- 좀비 프로세스 : 종료되었지만 커널 상 남은 ps, fork 시 무조건 wait 사용 or 이중 fork or sigaction으로 해결

  1. 자식 ps가 exit로 끝나면 부모 ps서 wait으로 결과 받음

  2. 부모 ps서 wait이 없거나 늦게 호출할경우 커널이 자식 ps의 상태 보관

  3. 자식은 다끝났는데, 자식 상태를 계속 보관 -> 늘어나면 문제

- 이중 fork

  1. 부모 ps가 fork -> 자식 ps 생성

  2. 자식 ps fork-> 손자 ps 생성한 형태

  3. 자식 ps 종료시 wait로 손자 종료 코드 못받으므로 손자는 정리됨(좀비 ps x)

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

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

    if (argc != 3){
        fprintf(stderr, "Usage : %s <command> <arg>\n", argv[0]);
        exit(1);
    }
    pid = fork();
    if (pid < 0){
        fprintf(stderr, "fork(2) failed\n");
        exit(1);
    }
    // child process
    if (pid == 0){
        execl(argv[1], argv[1], argv[2], NULL);
        perror(argv[1]);
        exit(99);
    }
    else{
        int status;

        waitpid(pid, &status, 0);
        printf("child (PID=%d) finished; ", pid);
        if (WIFEXITED(status))
            printf("exit, status =%d\n", WEXITSTATUS(status));
        else if (WIFSIGNALED(status))
            printf("signal, sig=%d\n", WTERMSIG(status));
        else
            printf("abnormal exit\n");
        exit(0);   
    }
}

 

 

파이프

- 프로세스 끼리 연결한 스트림. 부모, 자식 ps간에 pipe로 연결

 #include <unistd.h>

- int pipe(int fds[2]) : 두 ps  파일 디스크립터 fds[0]은 읽기 fds[1]은 쓰기 전용

- 파일디스크립터복제 : int dup(int oldfd), int dup2(int oldfd, int newfd) - 복제로 생성한 fds반환, 새 fds는 기존 fds계속반영

- 3번 파이프를 0번으로 옮기기 : 1. close(0) : 0번 비우기, 2. dup2(3, 0) : fds 3을 fds0 복제, 3. close(3) : 기존 fds 3 해제

#include <stdio.h>

- 위 시스템 콜 파이프는 쓰기 힘듦, stdio서 쉬운거 제공

- FILE *popen(const char *command, const char *mode) : command 프로그램 실행, 파이프 연결, 스트림 반환

- int pclose(FILE *stream) : popen으로 fork, exec 한 자식 ps에 대해 wait 수행,  파일 스트림 해제

 

 

프로세스 관계

1. 부모, 자식 관계 : pstree로 조회 가능, 최 상위 systemd가 커널이 가장 먼저 실행하는 모든 ps 시작점

#include <sys/types.h>

#include <unistd.h>

-> pid_t getpid(void), pid_t getppid(void) : 자식, 부모 pid 취득

2. 타 ps 정보 조회 : ps 파일 시스템 /proc에서 확인 or pstree or ps

3. 프로세스 그룹 & 세션 : 프로세서 세션 - 단말 단위, 프로세스 그룹 - 파이프로 연결된 ps들 단위

 + ps 그룹 리더 : 처음 ps 그룹 만든 ps (최상위 부모) -> PID =PPID

 + ps 세션 리더 : 처음 세션 만든 프로세스, PID = SID(세션 ID)

4. 데몬 PS : 단말이 없는 ps -> 서버 동작용(단말 접속과 무관하게 동작함)

#include <unistd.h>

-> int setpgid(pid_t pid, pid_t pgid) : 프로세스 그룹 설정, 현재 ps로 새 psg 만들 경우 pid와 pgid 둘다 0 설정

-> pid_t setsid(void) : 새 세션 생성, 현재 ps가 세션 리더 * 현재 ps가 psg 리더인경우 세션 리더 설정 불가

 


1. pstree 예시 : WSL 터미널에서 해서 그런가 ps도 거의없고 보통 환경이랑 좀 다르다.

2. /proc에서 보는 ps 상태

3. ps 그룹, 세션 조회 - ps j

4. 모든 프로세스 및 데몬 프로세스 조회 : 데몬은 tty가 없어 ?로 표기됨

 

 

시그널

- 단말(사용자) 혹은 커널이 ps에 통지시 사용(인터럽트와 유사)

- 시그널 전달 시 동작

 1. 무시 (SIGCHLD 시그널, 자식 ps 종료시 전달) -> 무시 대신 시그널을 어떻게 처리(catch)할지 설정도 가능함 

 2. ps 종료 : ctrl + c으로 sigint 발생

 3. core덤프 작성후 종료  : sigsegv, 접근 불가한 메모리 접근시 세그먼테이션 폴트 발생, 코어 덤프 작성(메모리 스냅샷)

 

 

시그널 처리 API

#include <signal.h>

- int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact) : sig에대한 시그너 핸들러 act 지정, oldact는 예전핸들러 NULL해도됨

struct sigaction{
	/* sa_handler 혹은 sa_sigaction 둘중 하나만 씀*/
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t*, void*);
    sigset_t sa_mask;
    int sa_flags;
}

- act에 아래의 구조체 사용 해서 시그널 액션 등록. 한번 설정한 시그너 핸들러는 계속 유지, sa_flags에 SA_RESTART 추가시 시스템 콜 재시작(보통 사용), sa_mask로 차단할(블록할) 시그널 지정가능-핸들러 처리중엔 타 sig방지sigemptyset() 

//sigaction 예시
#include <signal.h>

typdef void (*sighandler_t)(int);

sighandler_t trap_signal(int sig, sighandler_t handler){
	struct sigaction act, old;
    
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_RESTART;
    if (sigaction(sig, &act, &old) < 0)
    	return NULL;
    return old.sa_handler;
}

 

#include <sys/types.h>

#include <signal.h>

- int kill(pid_t pid, int sig) : 시그널 전송 함수, 해당 pid를 가진 ps에 sig 전달, 이름이 sendsig가 맞으나 오래되서 그럼

- ctrl + c : sig int 전달하여 종료

 

 

 

디렉터리 처리

#include <unistd.h>

- char *getcwd(char *buf, size_t bufsize); : 현재 ps의 경로 출력

- int chdir(const char *path) : ps의 현재디렉터리 변경

 

 

환경변수 처리

- ps에 의해 전파되는 전역변수

- ex : PATH(명령어 찾을 경로), TERM(사용 단말), LANG(사용자 로케일), LOGNAME(로그인명), TEMP(임시파일경로), DISPLAY(x 윈도우 용 기본 디스플레이)

- 전역변수 environ(char** 타입)으로 접근 가능

#include <stdio.h>
#include <stdlib.h>

extern char **environ;

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

    for(p = environ; *p; p++){
        printf("%s\n", *p);
    }
    exit(0);
}

#include <stdlib.h>

- char *getenv(const char *name) : 해당 이름의 env 가져오기

- chat *putenv(const char *string) : 환경변수 등록 string은 "이름=값"형태

 

 

 

로그인 처리

1. systemd서 단말 갯수만큼 getty 명령어 수행

2. getty는 사용자 입력 기다림 -> login 명령어 시작

3. login cmd로 사용자 인증 -> 셀 시작

- getty 하는일 : 터미널을 open, read해서 사용자 정보 입력받고 login exec -> dup로 0~2번 fds를 새단말과 연결&login반복

- login 하는일 : 사용자 인증, /etc/passwd에 암호 저장(요즘은 보안을 위해 셰도 패스워드 사용)

- 셸 시작 : execl("/bin/sh", "-sh", ...); : 이 명령어로 로그인 셸 실행

https://jhnyang.tistory.com/110

 

+ Recent posts