0%

3.1 Branches in a Nutshell

간단히 말해서 ..

깃은 데이터를 변경 사항에 세트나 다른점으로 저장하지 않고 snapshot으로 저장한다. 커밋을 수행하게 되면 깃은 commit object를 생성한다. 각 파일에 대한 blobs를 생성하고, 파일과 디렉터리 구조를 나타내는 tree 객체를 생성한다. 커밋 객체는 루트 트리와 각 메타 데이터에 대한 포인터를 포함한다.

다른 변경사항을 커밋하면 커밋 객체는 그 바로 전에 커밋 객체를 가르키는 포인터를 갖는다.

깃의 디폴드 브랜치 이름은 main이다. 처음 커밋을 하면 main 브랜치는 너가 만든 커밋을 가르킨다. 매번 커밋할때마다 main 브랜치는 자동으로 마지막으로 변경한 커밋을 가르키게 된다.

새 브랜치 생성

새로운 브랜치를 생성하면 작업한 마지막 커밋을 가르키는 브랜치가 생성된다.

1
git branch testing

깃은 HEAD라는 포인터를 사용해서 현제 어떤 브랜치에 있는지 가르킨다. 이는 다른 버전관리 시스템과 다른 특징이다. 이것은 로컬 브랜치에서 현제 작업하고 있는 포인터이다. 브랜치를 생성해도 다른 브랜치로 스위칭 되는 것은 아니다.

git log--decorate 옵션을 붙혀서 브랜치가 어떤 커밋을 가르키는지 확인할 수 있다.

git log 는 항상 모든 분기를 표시하지 않는다. 깃은 우리가 관심 있을만한 커밋에 대한 log만 보여주기 때문에 현제 작업하고 있는 브랜치에 대한 log 를 보여준다. 따라서 명시적으로 다른 브랜치에 대한 로그를 보려면 git log testing 같이 입력해야 한다. 또는, git log --all 옵션을 사용한다.

1
2
3
4
$ git log --oneline --decorate
f30ab (HEAD -> master, testing) Add feature #32 - ability to add new formats to the central interface
34ac2 Fix bug #1328 - stack overflow under certain conditions
98ca9 Initial commit

Switching Branches

git checkout 명령어로 존재하는 브랜치로 바꿀 수 있다. 최신 버전에 깃에 경우 git switch를 통해서 바꾼다.

1
git switch testing

이 명령어는 HEAD를 master 브랜치에서 testing 브랜치로 옮긴다.

이제 다른 커밋을 하면

1
2
$ vim test.rb
$ git commit -a -m 'made a change'

master 브랜치와 testing 브랜치가 가르키는 곳이 다르고 현제 작업하고 있는 포인터인 HEAD에 위치도 testing 브랜치에서 변경한 마지막 커밋으로 바뀐다.

이제 다시 switch 명령을 통해 브랜치를 master로 바꿔 보자.

1
git switch master

이 명령을 입력하면 HEAD를 master 브랜치로 옮기고 파일을 마스터 브랜치가 있는 시점으로 복구한다. 이것은 예전 버전으로 복구 하는 것의 분기를 나눈 것과 같다. 또한 testing 브랜치에서 작업한 내용을 저장하고 있기 때문에 언제든지 작업한 내용으로 이동할 수 있다.

브랜치를 이동하면 워킹 디렉토리에 파일이 변경된다.

커밋을 한번 더 해보자.

1
2
$ vim test.rb
$ git commit -a -m 'made other changes'

그러면 다른 작업으로 독립되어 분리된다. 이것은 testing 에서 작업하던것을 임시로 미루고 main에서 작업한 다음 다시 testing 브랜치로 돌아와 작업할 수 있음을 의미한다. 이렇게 작업하고 필요할때 merge 하는 등에 작업을 해서 하나로 합치면 된다.

git log --oneline --decorate --graph --all를 사용하면 브랜치, 분기, 포인터 history등에 정보를 한눈에 볼 수 있다.

1
2
3
4
5
6
7
$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) Made other changes
| * 87ab2 (testing) Made a change
|/
* f30ab Add feature #32 - ability to add new formats to the central interface
* 34ac2 Fix bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project

실제로 Git의 브랜치는 어떤 한 커밋을 가리키는 40글자의 SHA-1 체크섬 파일에 불과하기 때문에 만들기도 쉽고 지우기도 쉽다. 새로 브랜치를 하나 만드는 것은 41바이트 크기의 파일을(40자와 줄 바꿈 문자) 하나 만드는 것에 불과하다.

부모 정보도 알고 있어서 merge base를 어떤걸로 해서 merge 할지 자돋으로 알기 때문에 머지도 가볍게 잘 된다.많이 생성하고 많이 병합하세요.

git checkout -b <newbranchname> 명령어를 사용해서 한번에 생성과 switch 가지 할 수 있다.

리눅스 사용법 익히기

리눅스 설치 방법

  1. 가상 머신 소프트 웨어
    • PC 환경에 따라 문제들이 다름.
  2. 클라우드 컴퓨팅 서비스 활용
    • 최신 기술에 익숙해 짐.
    • 동일한 환경에서 사용이 가능

클라우드 컴퓨팅

서버 환경을 미리 구축해 놓고, 간단한 설정으로 바로 사용할 수 있도록 만든 서비스.

  • 이전 : 미리 서버 컴퓨터를 구축해야 함. 예측해서 구축해야 되는 문제
  • 현재 : 클라우드 컴퓨팅 환경 사용

AWS 회원 가입

무료로 1년간 사용 가능 함.

해외 결제 가능한 신용카드가 필요하다.

AWS 클라우드 컴퓨팅 설정

  1. EC2 또는 인스턴스(서버) 생성
  2. Elastic IP (탄력적 IP) 생성
    • 고정 IP로 만들어야 함.
  3. 자기 PC(클라이언트) 에서 EC2(서버) 접속

EC2 > 인스턴스 > 인스턴스 시작 > 우분투 18.04 > 옵션 선택 후 생성

태그 추가? 실제 현업에서는 다수의 서버를 운용하니까 태그로 이름 구분

고정 IP 생성 및 연결

AWS 콘솔에서 탄력젹 IP 선택 후 새로운 IP 할당.
할당한 IP와 새로운 기존에 인스턴스를 연결 .

터미널을 통한 우분투 접속.
기존에 받아 놓은 pem 이 있는 폴더로 이동

1
chmod 400 <user-key.pem>

받아놓은 키에 권한을 변경해 줌.

ssh 를 이요한 접속.

1
ssh -i <user-key.pem> ubuntu@<IP>

ubuntn 란 사용자로 해당 IP로 접속…

참고로 고정 IP는 할당만 하고 연결 안하면 비용이 청구 된다.
또 연결 했더라도 실행을 시키지 않으면 비용이 청구 되고 여러개의 고정 IP를 할당해도 비용이 청구된다.

리눅스와 파일

모든 것은 파일이라는 철학을 따름

  • 모든 인터렉션은 파일을 읽고, 스는 것처럼 이루어짐. (마우스, 키보드와 같은 모든 디바이스 관련 동작도)

  • 파일 네임 스페이스

    • 전역 네임스페이스를 사용함 (/media/floofy/dave.jpg)
  • 파일은 inode 고유값과 자료구조에 의해 주요 정보를 관리한다.

리눅스와 프로세스

  • 리눅스는 ELF 실행 파일 포멧을 갖는다.
  • 다양한 시스템 리소스와 관련 되어 있다.
  • 가상메모리를 지원한다.
  • 프로세스는 PID 기반으로 구분된다.
  • init 프로세스를 기반으로 fork() 시스템콜을 사용해서, 신규 프로세스가 생성 된다.

리눅스와 권한

운영체제는 사용자 리소스 권한을 관리한다.
리눇는 사용자 / 그룹으로 권한을 관리한다.
root는 슈퍼 관리자이다.
파일마다 소유자, 소유자 그룹, 모든 사용자에 대한 읽고, 쓰고, 실행되는 권한을 관리한다.
접근 권한 정보는 inode의 자료구조에 저장된다.

리눅스

서버에 많이 사용되는 운영체제

프로그래밍 할때에도 많이 사용 된다.
컴파일 시간이 줄어든다.

클라우드 컴퓨팅 (AWS) 에도 많이 사용됨

운영체제, 소프트웨어의 대부 UNIX 계열 운영체제이다.
완전 프로그래머 스타일이다.

plain 하게 프로그래밍 가능하다.
ANSI C - C 언어 표준 (표준으로 코딩이 가능하다.)


리눅스의 역사와 배경

리눅스 시작

리누스 토발즈가 개발.
대학에 있는 UNIX 컴퓨터를 집에서 쓰고 싶었다.
다중 사용자, 다중 작업(시분할 시스템, 멀티 태스킹) 을 지원할 수 있도록 만들었다.

GNU 프로젝트

GNU = Gnu is Not Unix

유닉스 운영체제를 여러 회사에서 개발했는데, 소스를 공유하지 않았다.
리차드 스톨만이 초기 컴퓨터 개발 공동체의 상호협력적인 문화로 돌아가자고 1985년 GNU 선언문을 발표했다.
기술을 공유 되어야 한다고 주장.

GNU 프로젝트를 지원하기 위해 자유 소프트웨어 재단 설립 GNU 공개 라이선스(GPL) 규약을 제공.
이 소스를 사용할수 있지만 해당 기술을 사용했다면 기술은 공유 되어야 한다는 규약.

GPL 라이선스

GPL 프로그램은 어떤 형태로든 사용할 수 있지만, 다시 배포하기 위해서 동일한 GPL 라이선스 사용해야 함.

GNU Hurd

운영체제 커널 개발 시도 - GNU Hurd

운영체제의 필요한 쉘 응용 프로그램, API, 라이브러리 System Call 개발, 컴파일러 개발

GNU 프로젝트와 리눅스

GNU Hurd 개발 지연

리누스 토발즈가 리눅스 커널 소스 오픈 소식 들음. GNU Hurd 를 개발 안해도 되겠네?

GNU 프로젝트 산출물과 리눅스 커널이 통합 개발 되기 시작.

GNU/ Linux 라고 부르기를 희망함. 리차드 스톨만이 (GNU 창시자.)

shell

쉘 - 사용자와 컴퓨터 하드웨어 또는 운영체제 간의 인터페이스

쉘 종류

  • bash
  • sh
  • csh
  • ksh

다중 사용자 관련 명령어

whoami - 사용자 이름 출력

passwd - 새로운 패스워드 설정
(aws 에서 만들었으면 이미 설정된 아이디 사용하므로 패스워드가 없음)
root 권한만 사용할 수 있음.

useradd는 사용자 기본 설정 자동으로 하지 않음.

adduser는 사용자 기본 설정을 자동으로 함. (이거 사용해) - root 사용자가 만들 수 있음

sudo root 권한으로 실행하기
(해당 id 가 sudo 를 사용할수 있도록 미리 설정되어야 함.)

su 사용자 변경

- `su root`  현재 사용자의 설정 파일을 기반으로 id만 바
- `su - root` 변경되는 사용자의 환경설정을 기반으로 id도 바뀜 

sudo

/etc/sudoers 설정 파일에서 설정을 해주어야 함.
root 권한이어야 읽을 수 있고 수정할 수 있음.
새로 생성한 id에 sudo 사용 권한을 주고 싶다면
dave2 ALL=(ALL:ALL) ALL

  1. 특정 사용자가 sudo를 사용할 수 있도록 userid ALL=(ALL) ALL
  2. 특정 그룹에 포함된 모든 사용자가 sudo를 사용할 수 있도록 %group ALL=(ALL) ALL
  3. 패스워드 생략 설정 %group ALL=(ALL) NOPASSWD: ALL

pwd 현재 디렉토리 위치

cd 디렉토리 이동 cd - 바로 전 디렉토리로 이동

dir 파일 목록 ls 랑 똑타음 플래그 사용하려면 ls 사용하는게 나음.

와일드 카드

  • * 는 임의 문자열
  • ? 는 문자 하나

man man 띄고 명령어 입력하면 플래그 정보를 알 수 있음.

ls와 파일 권한

리눅스는 파일마다 소유자, 소유자 그룹, 모든 사용대한 읽고, 쓰고, 실행하는 권한을 설정한다.

- rwx rx- r-x

  • 처음 1칸 데이터가 파일이면 -, 폴더면 d
  • 다음 3칸 소유자의 권한
  • 다음 3칸 그룹의 권한
  • 다음 3칸 기타 상자의 권한

r : 읽고, 카피 (ls 명령어 가능)

w : 수정 (파일 생성 가능)

x : 실행 (cd 접근 가능)

chmod : 파일 권한 변경

1
2
3
4
chmod g+rx test.c
chmod u+rx test.c
chmod ug+rx test.c
chmod u=rwx, g=rw, o=rx test.c

숫자를 사용하는 방법

1
2
3
4
rwx rwx rwx = 777
r-x r-x r-x = 555
r-- --- --- = 400
rwx --- --- = 700

chown 소유자 변경

cat 파일 내용 보기

head 앞부분 보기

tail 끝 부분만 보기

more 화면에 보일 수 있는 부분만 보고 나머지는 스크롤 하면서 보겠다.

rm remove 에 약자

  • r: 하위 디렉토리를 포함한 모든 파일 삭제
  • f: 강제로 파일이나 디릭토리 삭제

rm -rf [dir]

리다이렉션과 파이프

Stansdard Stream(표준 입출력)

command로 실행되는 프로세스는 세가지 스트림을 가지고 있다.

  • 표준 입력 스트립(Standard Input Stream) - stdin
  • 표준 출력 스트립(Standard Output Stream) - stdout
  • 오츄 출력 스트림 (Standard Error Stream) - stderr

모든 스트림은 일반적인 plain text로 consol에 출력하도록 되어 있음.

리다이렉션(redirection)

표준 스트림 흐름을 바궈준다.
주로 표준 출력을 화면이 아닌 파일로 할때 사용한다.
혹은 파일에 있는 내용을 응용 프로그램에 입력으로 바꿔 줄때 사용하기도 한다.

1
2
3
ls > files.txt
head < files.txt
head < files.txt > files2.txt

head < files.txt 원래는 이게 화면에 출력 되어야 하지만 뒤에 > files2.txt 가 있기 때문에 리다이렉션 되어서 files2.txt 에 해당 내용이 적힌다.

ls >> files.txt 기존에 파일이 있으면 append 되어서 추가된다.

파이프 (pipe)

두 프로세스 사이에서 한 프로세스의 출력 스트림을 또다른 프로세스의 입력 스트림으로 사용하는 기능.
기호로 |를 사용한다.

유닉스의 철학 : 프로세스를 간단하게 만듦.

ls | grep files.txt : grep 은 입력으로 들어온 데이터 중에서 인수로 받은 텍스트를 찾는 명령어이다.
파일 리스트에서 특정한 파일만 찾을때 사용한다.

grep flag

리눅스 파일 시스템과 쉘 명령어

리눅스는 I/O 를 파일 처럼 처리한다.

가상 파일 시스템을 사용한다.

예를들어, tty 는 가상파일 시스템 인퍼에스로 가상 터미널 환경과 연결되어 있다.
키보드 인풋을 받는다.

슈퍼 블록, inode와 파일

슈퍼 블록 : 파일 시스템의 정보

파일 : inode 고유값과 자료구조에 의해 주요 정보를 관리한다.

  • 파일 이름:inode 파일이름은 inode 번호와 매칭
  • 파일 시스템에서는 inode를 기반으로 파일 엑세스
  • inode 기반 메타 데이터 저장. (프로세스에서 PCB와 비슷하다.)

메타 데이터에는 파일 권한, 소유자 정보, 파일 사이즈, 생성 시간등 시간 관련 정보, 데이터 저장 위치 같은 정보가 적힌다.

파일과 inode

각 디렉토리에는 dentry 리 라고 하는 엔트리 정보가 있음.
/home/ubuntu/link.txt 파일의 경우 / dentry 에서 home 서브 디릭토리를 찾고, home 에서 unbuntu를 찾고, ununtu에서 link.txt를 찾음.

디렉토리도 마찬가지로 inode를 갖고 있으므로 이를 통해 구별할 수 있다.

하드 링크와 소프트 링크

cp 파일 복사 : -rf 옵션을 많이 사용함. 폴더 안쪽 까지 모두 복사함.
cp -rf programing/ programing2

rm : 파일 삭제. rm -rf 로 많이 사용함.

바로가기 처럼 만들고 싶다? -> 링크 하고 싶다.

하드링크

ln link.txt hard.txt link.txt를 링크하는 hard.txt

하드 링크는 또다른 파일을 만드는 것인데 inode 구조체는 동일하게 된다. 복사와 다른 점은 복사는 inode 구조체까지 복사한다.

ls -i 로 inode 값 확인 링크로 만들면 inode id 값이 같다.

만약 link.txt를 변경하면 hard.txt 도 똑같이 변경된다.

하드링크에 경우에는 원본 파일이 사라지는 경우에도 파일 내용은 그대로이다. 이것은 실제 파일 하고 가르키고 있는 inode 구조체가 분리 되어 있기 때문이다.

이렇게 하면 좋은점? inode를 공유 하면서도 파일 복사 같이 되기 때문에 inode 구조체를 또 복사할 필요가 없다.

소프트 링크 (심볼릭 링크)

윈도우에 바로가기 아이콘과 같다고 할 수 있다.
원본 파일이 없어지면 접근이 안된다.

소프트 링크는 indoe 값이 다르다. 소프트 링크는 l로 시작한다. 링크 파일임으로 원본 파일에 내용이 바뀌면 소프트 링크에 내용 도 바뀐다. but, 원본이 삭제되면 소프트 링크 파일도 사라진다.

소프트 링크 같은 경우에는 별도의 inode structure을 갖는다. 다만, address 로 direct Block 을 갖는 것이 아니라 원본 파일을 가르키는 redirected path 를 갖는다.

특수 파일

리눅스는 모든것을 파일처럼 다루기 때문에 디바이스도 파일처럼 다룬다.

  • 블록 디바이스 (Block Device) : 저장 매체 등은 블록 단위로 데이터를 읽거나 써야 효율적이다. I/O 송수신 속도가 높다.
  • 캐릭터 디바이스 (Charcter Device) : 캐릭터 디바이스는 굳이 블록단위로 데이터를 보내지 않아도 되는 경우로 byte 단위로 데이터를 전송하고 키보드나 마우스 같은 경우가 있다. I/O 송수신 속도가 낮음.

b 로 시작하면 블록 디바이스 이다.

c 로 시작하면 캐릭터 디바이스 이다.

시스템 프로그래밍 핵심 기술

시스템 콜과 API

시스템 프로그래밍의 기반 요소

  • 시스템 콜
  • C 라이브러리
  • C 컴파일러
    사용자 영역에서 시스템 프로그래밍을 해본다.

시스템 콜

커널 영역으로 들어가는 함수.

리눅스 유닉스도 C 언어로 만들어져 있으며, 시스템 콜도 C 언어로 구현한다.

시스템 콜은 어떻게 구현?

eax 에 시스템 콜 번호를 넘겨 준다.
ebx 에는 시스템 콜 인자를 넘겨준다.
INT 로 인터럽트를 실행해서 0x80 에 해당아하는 함수를 실행 시키는데 IDT(Interrupt Descriptor Table) 에서 0x80은 리눅스에서 시스템 콜에 해당한다.

1
2
3
mov eax, 1
mov ebx, 0
int 0x80

이때 커널 모드로 바뀌어서 실행되고, 실행이 완료 된 다음에는 다시 사용자 모드로 바뀐다.

API

응용 프로그램과 불리된 하위 호환 인터페이스. 예) 시스템 콜 래퍼, 입출력 라이브러리 등등..

ABI와 표준

C 라이브러리

유닉스 - libc

리눅스 - glibc

이 안에 시스템 콜, 시스템 콜 래퍼, 기본 응용 프로그램 기능이 포함 된다.

C 컴파일러

유닉스 - cc

리눅스 - gcc

1
2
3
sudo apt-get install gcc
gcc --version
gcc -o test.c test // 실행 파일이 생성

만약 -o 안쓰면 a.out으로 실행파일 생성됨.

ABI

Application Binary Interface

  • 함수 실행 방식, 레지스터 활요으, 시스템 콜 실행, 라이브러리 링크 방식등을 제공 한다.
  • ABI가 호환되면 재 컴파일없이 동작한다.
  • 컴파일러, 링커(라이브러리 링크), 툴체인(컴파일러를 만드는 프로그램) 에서 제공한다.

각각의 업체에서 이것을 정의해서 표준화 할 필요가 있었다.

POSIX

유닉스 시스템 프로그래밍 인터페이스 표준이다.
IEEE에서 표준화를 계속해서 시도하고 있고, 리차드 스톨만이 POSIX를 표준안 이름으로 제안했다.

C 언어 표준

다양한 C 언어의 변종이 존재한다. 그래서 ANSI에서 ANSI C 표준을 정립했고 리눅스에서는 POSIX 와 ANSI C 를 지원한다.

시스템 프로그래밍과 버전

하위 시스템 레벨은 거의 변하지 않고 유지되고 있음. 하위 레벨까지 잘 알고 있으면 상위 레벨에서 동작하는 프로그램을 만들지라도 성능 향상에 도움을 줌.

프로세스 관리

프로세스 ID

각각의 프로세스는 proess id 라고 하는 PID 를 갖는다.

  • pid : 해당 시점에 unique한 pid
  • 최대 값은 2의 15승인 32768( 부호형 16비트 정수값)
  • ppid : 부모 프로세스의 pid 값

ps -ef를 통해 pid, ppid 및 프로세스스 정보 확인

vi /etc/passwd를 프로세스와 소유자를 관리함.

예시

1
root:x:0:0:root:/root:/bin/bash
  • 사용자명 : root
  • 패스워드 : x (없음)
  • 사용자 ID : 0
  • 그룹 ID : 0
  • 사용자 정보 : root
  • 홈 디렉토리 : /root
  • 쉘 환경 : /bin/bash

pid 및 ppid 확인

1
2
3
4
5
6
7
8
9
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main() {
printf("pid=%d\n", getpid());
printf("ppid=%d\n", getppid());
return 0;
}
1
2
3
4
5
$ gcc test.c -o test
$ ./test

pid=22440
ppid=22164

프로세스 생성

기본 프로세스는 text, data, bss, heap, stack의 공간을 생성 한 후 프로세스 이미지를 해당 공간에 업로드하고 실행을 시작한다.

fork() 와 exec() 시스템콜

  • fork() : 새로운 프로세스 공간을 별도로 만들고, fork() 시스템 콜을 호출한 프로세스 (부모 프로세스) 공간을 모두 복사
    • 별도의 프로세스 공간을 만들고, 부모 프로세스 공간의 데이터를 그대로 복사( 부모 프로세스는 그대로 살아 있음.)
  • exec() : 호출한 현재 프로세스 공간의 text, data, bss 영역을 새로운 프로세스의 이미지로 덮어씌움
    • 별도의 프로세스 공간을 만들지 않음. (덮어 씌워 지는 거라 부모 프로세스 없음.)

fork

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(){
pid_t pid;
printf("Before fork() call\n");
pid = fork();

if (pid == 0)
printf("this is Child process. PID is %d\n", pid);
else if (pid > 0)
printf("this is Parent process. PID is %d\n", pid);
else
printf("fork() is failed\n");
return 0;
}

fork()가 실행되면 부모 프로세스와 동일한 자식 프로세를 별도의 메모리 공간에 생성한다. 자식 프로세스에 pid 값은 0으로 리턴된다. 이때 동일한 pc 값을 가지기 때문에 이후에 코드가 실행되고 위에서 예시에선 pid > 0 일때 코드 가 출력되고 다음으로 pid == 0 일때 코드가 출력된다.

exec()

인자로 덮어씌워질 프로세스 정보를 넘겨준다. exec는 여러가지 함수 패밀리들이 있다. pc 다음부터는 코드가 덮어씌워지기 때문에 제대로 동작했으면 exec를 후출한 다음줄 부터는 실행이 안될 것이다.

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
printf("execute ls\n");
execl("/bin/ls", "ls", "-l", NULL);
perror("execl is failed\n");
exit(1);

}

execl은 첫번재 인수로 디렉토리 이름이 명시된 명령어를 받고 다음부터 명령어 플래그등을 인수로 받는다. 갯수는 상관 없이 받을 수 있지만 마지막 인수는 NULL 로 끝나야 한다.
실행 시키면 perror 부분에 코드는 실행되지 않고 덮어씌워진 ls 명령이 실행된다.

execlp 디폴트 환경변수 패스 값을 사용하기 때문에 명령어만 넘겨주면 된다. execlp("ls", "ls", "-l", NULL)

execle() 환경변수를 지정하고 넘겨준다.

1
2
char *envp[] = {"user=dave", "PATH=/bin", (char *)0};
execle("ls", "ls", "-al", NULL, envp);

execv는 인자로 변수로 만들어서 넣을 수 있다.

1
2
char *arg[] = {"ls", "-al", NULL};
execv("/bin/ls", arg);

fork와 exec

fork : 부모 프로세스로부터 새로운 프로세스 공간을 만들고 부모 프로세스 데이터를 복사(fork)

exec : 새로운 프로세스를 위한 바이너리를 새로운 프로세스 공간에 덮어 씌움 (exec)

wait() 시스템 콜

wait() 함수를 사용하면, fork() 함수 호출시, 자식 프로세스가 종료할 때까지, 부모 프로세스가 기다림. 브모 프로세스가 자식 프로세스보다 먼저 죽는 경우를 막기 위해 사용된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int pid;
int child_pid;
int status;
pid = fork();
switch(pid) {
case -1:
perror("fork is failed\n");
break;
case 0:
execl("/bin/ls", "ls", "-la", NULL);
perror("execl is failed\n");
break;
default:
child_pid = wait(NULL);
printf("ls is complete\n");
printf("Parent PID (%d), Child PID (%d)\n", getpid(), child_pid);
exit(0);

}
}

execl를 호출하면 부모 프로세스가 덮어씌어 지니까 fork를 통해 자식 프로세스 생성. case 0 인 경우는 자식 프로세스 인 겨으로 여기에 ls 명령어를 덮어씌움.

ls 명령어 출력

wait()를 통해서 자식 프로세스가 끝나기를 기다렸다가 부모 프로세스를 실행 시킬 수 있음.

copy-on-write

리눅스에 프로세스 크기는 4GB에 크기를 가지는데 fork()로 복제할때마다 전체를 복사하려고 하면 오래 걸린다.
따라서 자식 프로세스를 생성했다면 우선은 부모 프로세스에 페이지를 같이 굥유하는 방법으로 생성한다.
그리고 페이지를 읽는 것이 아니라 써야 할때 페이지를 복사하고 분리하는 방법을 사용한다.

장점

  • 프로세스 생성 시간을 줄일 수 있다.
  • 새로 생성된 프로세스에 새롭게 할당되어야 하는 페이지 수도 최소화 된다.

프로세스 종료

exit() 세스템콜 : 프로세스 종료

c 언어에서 main 함수에서 return 을 호출하는 것과 exit()을 호출하는 것에는 어떤 차이가 있을까?

c 언어 실행 파일은 우선 _start() 함수를 호출하고 그 안에서 main() 함수를 호출함. return 하면 main 함수가 끝나고 다음 exit()함수를 호출함.

만약 main 함수에서 exit() 함수를 호출했다면 바로 프로세스를 종료시키는 것임.

exit() 시스템 콜

부모 프로세스는 status & 0377 계산 값으로 자식 프로세스 종료 상태 확인 가능

기본 사용

1
2
exit(EXIT_SUCCESS);
exit(EXIT_FAILURE);

주요 동작

  • atexit() 에 등록된 함수 실행
  • 열려 있는 모든 입출력 스트림 버퍼 삭제
  • 프로세스가 오픈한 파일을 모두 닫음
  • tempfile() 함수를 생성한 임시 파일 삭제

atexit() 함수

프로세스 종료시 실행될 함수를 등록하기 위해 사용한다. 등록된 함수를 등록된 역순서대로 실행한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void){
void exithandling(void);
void goodbymessage(void);
int ret;

ret = atexit(exithandling);
if (ret != 0) perror("Error in atexit\n");
ret = atexit(goodbymessage);
if (ret != 0) perror("Error in atexit\n");
exit(EXIT_SUCCESS);
}

void exithandling(void) {
printf("exit handling\n");
}

void goodbymessage(void) {
printf("see you again!\n");
}

역순이기 때문에 goodbymessage 가 먼저 호출 됨.

wait() 시스템 콜

  • wait() 함수를 사용하면 fork()로 생성된 자식 프로세스가 종료될때까지 부모 프로세스가 기다림
  • 자식 프로세스가 완전히 끝나면 좀비 프로세스가 가지고 있던 최소 정보도 삭제하고 부모 프로세스에 SIGCHLD 시그널이 보내짐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int pid;
int child_pid;
int status;
pid = fork();
switch (pid) {
case -1:
perror("fork is failed\n");
break;
case 0:
execl("/bin/ls", "ls", "-al", NULL);
perror("execl is failed\n");
break;
default:
child_pid = wait(&status);
if (WIFEXITED(status)) {
printf("Child process is normally terminated\n");
}
exit(0);
}
}

wait의 리턴값은 종료된 자식 프로세스의 pid 값임.

status 정보를 통해 기본적인 자식 프로세스 관련 정보를 확인할 수 있음

1
int WIFEXITED(status);

자식 프로세스가 정상 종료 시 리턴값은 0 이 아닌 값이 됨.

IPC 기법관련 시스템콜 사용법 이해

파이프

파이프를 통한 부모 -> 자식 간 단방향 통신

예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define MSGSIZE 255

char* msg = "Hello Child Process!";
int main() {
char buf[255];
int fd[2], pid, nbytes;
if (pipe(fd) < 0) // pipe(fd) 로 파이프 생성
exit(1);
pid = fork(); // 이 함수 실행 다음 코드부터 부모/ 자식 프로세스로 나뉘어짐
if (pid >0) { // 부모 프로세스에는 자식 프로세스 pid 값이 들어감
printf("parent PID:%d, child PID%d\n", getpid(), pid);
write(fd[1], msg, MSGSIZE); // fd[1] 에 씁니다.
exit(0);
}
else { // 자식 프로세스에는 pid 값이 0이 됨.
printf("child PID: %d\n", getpid());
nbytes = read(fd[0], buf, MSGSIZE); // fd[0] 으로 읽음
printf("%d %s\n", nbytes, buf);
exit(0);
}
return 0;
}

메시지 큐(message queue)

FIFO 정책을 데이터 전송.

전송하는 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/msg.h>

typedef struct msgbuf {
long type;
char text[50];
} MsgBuf;

int main(void) {
int msgid, len;
MsgBuf msg;
key_t key = 1234;
msgid = msgget(key, IPC_CREAT|0644);
if(msgid == -1) {
perror("msgget");
exit(1);
}
msg.type = 1;
strcpy(msg.text, "Hello Message Queue\n");
if(msgsnd(msgid, (void *)&msg, 50, IPC_NOWAIT) == -1) {
perror("msgsnd");
exit(1);
}
return 0;
}
  • 동일한 key를 사용해야 함.
  • msgget 으로 msgid 를 얻음.
  • msgsnd 으로 큐에 메세지 전송

받는 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <sys/msg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

typedef struct msgbuf {
long type;
char text[50];
} MsgBuf;

int main(void) {
MsgBuf msg;
int msgid, len;
key_t key = 1234;
if((msgid = msgget(key, IPC_CREAT|0644))<0) {
perror("msgget");
exit(1);
}
len = msgrcv(msgid, &msg, 50, 0, 0);
printf("Received Message is [%d] %s\n", len, msg.text);
return 0;
}
  • 동일한 key 사용
  • msgid 를 얻음
  • msgrcv를 통해 메세지큐에 데이터 받음.

공유 메모리(shared memory)

커널 공간에 메모리 공간을 만들고, 해당 공간을 변수처럼 쓰는 방식. mesage queue 처럼 FIFO 방식이 아니라, 해당 메모리 주소를 마치 변수처럼 접근하는 방식이다. 공유 메모리 key를 가지고 여러 프로세스가 접근 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(void) {
int shmid, pid;
char *shmaddr_parent, *shmaddr_child;
shmid = shmget((key_t)1234, 10, IPC_CREAT|0644);
if(shmid == -1) {
perror("shmget error\n");
exit(1);
}
pid = fork();
if (pid >0) {
wait(0);
shmaddr_parent = (char *)shmat(shmid, (char *)NULL, 0);
printf("%s\n", shmaddr_parent);
shmdt((char *)shmaddr_parent);
}
else {
shmaddr_child = (char *)shmat(shmid, (char *)NULL, 0);
strcpy((char *)shmaddr_child, "Hello Parent!");
shmdt((char *)shmaddr_child); // 해당 프로세스에서 메모리에 올라간 변수 삭제
exit(0);
}
shmctl(shmid, IPC_RMID, (struct shmid_ds *)NULL); // 공유 메모리에서 삭제
return 0;
}