1991년 첫 버전이 '대학원생'에 의해 개발되어 공개되었다. 역시 대학원생은 무엇이든 할 수 있다.
전세계 컴퓨팅 기기의 과반수가 리눅스를 기반으로 개발되었다.
리눅스는 완전히 공개된 운영체제로 무료로 이용할 수도 있다. 또한 유닉스와 호환이 완벽하며 네트워크와 임베디드에서 강한 모습을 보인다. 많이 사용되는만큼 빠르게 진화하고 변화하는 것이 장점이자 단점이다.
커널과 사용자를 연결해주는 인터페이스인 쉘을 사용하여 CLI로 대화한다.
쉘 프로그래밍에서의 유용했던 명령어들은 다음과 같다.
1. man, 명령어 --help
매뉴얼이나 도움말을 호출해준다. 존재하지 않을 경우엔 안뜨지
2. 파일+디렉터리
2-1. ls -옵션 -전달인자 - 현재 디렉토리의 내용 출력
2-2. cd - 후술된 디렉토리로 이동
2-3. mkdir/rmdir 디렉토리 생성/삭제, 삭제시 비어있어야함(rm으로 대체)
2-4. pwd - 지금 있는 디렉토리
2-5. touch - 수정시간 갱신(빌드할 때 사용 여부) 혹은 빈 파일 생성
2-6. file -
2-7. cp 소스 타깃- 파일 복사
2-8. mv - 파일 이동, 이름 변경
2-9. rm - 파일 삭제 -r 시 재귀삭제(하위 디렉토리 모조리 삭제)
2-10. ln -
2-11. du -
2-12. find -
2-13. cat - 파일 연결 (cat a.c a.c>b.c 하면 a.c와 b.c 내용 연결, cat a.c 하면 a내용 출력)
2-14. head/tail -n 소스 - 앞/뒤 몇 줄만 보기
2-15. more/less - 파일 화면에 한 페이지씩 출력
2-16. xxd -
3. 프로세스
3-1. ps - 현재 실행중인 프로세스
3-2. top -
3-3. kill - 프로세스 강제 종료
4. 사용자
4-1. sudo - 최고 권한으로 실행
4-2. useradd -
4-3. userdel -
4-4. passwd -
5. 권한
5-1. chmod u+x(or 777) 소스- 읽기/쓰기/실행 권한 변경,-rwxrwxrwx(소유자 소유그룹 그밖 순) 비트로 수정 가능
5-2. chown - 소유자 변경
5-3. chgrp - 소유 그룹 변경
6. 압축
6-1. tar - 압축 해제(zxvf) 관리
7.네트워크
7-1. telnet -
7-2. ifconfig - 현재 네트워크 정보
7-3. netstat -
8. 그 외
>, < - Redirection. 표준 입출력을 특정 파일로 재지정할 수 있다. (ls -a>ls.txt 에 저장)
| - 명령 연결, 앞 명령의 결과가 뒤 명령의 입력값이 됨 (ls -a | sort | grep conf)
쉘 스크립트 .sh 에서 자주 쓰던 명령어는 다음과 같다.
!/bin/bash - 사용할 쉘 프로그램 (경로 포함) 지정
./*.sh - 쉘에서 스크립트 실행
echo - printf
$ - 변수 앞에 붙여서 값 사용
' ' - 변수의 내용으로 치환하지 않고 그대로
$1, $2, ... - 스크립트 실행시 전달된 argument 접근방법
if [ 조건=조건 ]; then ~ else ~ fi - 조건문, []의 시작과 끝은 항상 공백이 있어야 한다.
while [ 조건=조건 ]; do ~ done - 반복문
func() { ~~ } - func로 호출
VI Editor 에서 자주 사용하던 명령어는 다음과 같다.
1.Normal Mode
방향키 - 줄 이동 및 칸 이동
G - 문서 마지막으로 이동
gg - 문서 처음으로 이동
yy - 현재 줄 복사
2yy - 현재부터 2줄 복사
p - 커서 다음위치 붙여 넣기
P - 커서위치 붙여넣기
x - 커서 글자 삭제
dd - 현재 행 삭제
2dd- 현재 행부터 2줄 삭제
i - Insert Mode로 변경
u - ctrl + z
ctrl + r - ctrl + y
2.Insert Mode
하던대로 하면 됨
3.Command Mode
/찾는단어 - 단어 찾기
:%s/원래단어/바꿀단어/g - 단어 일괄 바꾸기
:w - 저장
:q - vi 종료
ZZ - 저장 후 종료
:q! - 저장없이 강제종료
:wq - 저장후 종료
:wq! - 저장후 강제종료
리눅스
프로세스
프로세스는 실행중인 프로그램이다.
리눅스는 멀티 태스킹을 지원하기 때문에 동시에 여러 프로세스를 실행할 수 있고, 커널과 커널에서 실행된 init 과 kthreadd가 자식 프로세스로 기본적으로 실행된다.
프로세스는 각자 독립적인 주소 공간을 갖고 있다. 프로세스가 사용할 메모리의 주소만 갖고 있는게 아니라 사용할 수 있는 외부 장치의 주소 또한 저장하고 있다. 물론 이 주소 공간은 리눅스 운영체제를 위협하지 않도록 할당받으며, 실제 물리적인 주소가 아닌 가상의 주소를 가진다. 이 가상의 주소는 실제 물리적인 주소 공간에 매핑되어 사용할 수 있으며 같은 물리 공간에 여러 가상 주소를 매핑할 수도 있다. 물론 이런 상황에서도 프로세스는 각자 독립적인 주소 공간을 갖고 있다고 여겨지며 이 때문에 프로세스간 변수를 공유할 수 없다.
프로세스를 생성하고 실행하는 방법은 여러가지 있다.
그 전에 return status 를 먼저 알아두자.
return status 는 exit code(8b) + signal number(8b)로 이루어져있다. 둘 중 하나만이 유효하며 유효한 정보는 0이 아닌 특정 숫자를 가진다. return status 는 16b 숫자로 표현하는데 만약 0x1100 이라면 exit code 11 signal number 00인 상태이며 main에서 종료됐거나 exit()에 의해 종료됐을 경우를 의미한다. signal number가 유효하다면 특정 시그널에 의해 종료된 상태를 의미한다.
system(const char * command)
프로세스에서 새로운 프로세스를 실행시키며, 새로운 프로세스가 완전히 종료될 때까지 기존의 프로세스는 대기상태가 된다. 하지만 백그라운드에서 실행시키면(... &) 기다리지 않고 두 개의 프로세스가 실행된다. system()으로 실행시 SIGCHLD 와 SIGINT SIGQUIT은 무시되기 때문에 잘못 만들었을 경우 기존 프로세스가 영원한 sleep상태에 빠질 수 있고, 쉘을 이용하므로 비효율적(왜?)이고 시스템 의존적이다.
exec()
exec에서 파생된 함수들이 다양하게 존재한다.(manual참고) 기본 기능은 현재 프로세스를 새로운 프로세스로 교체한다. 자주쓰던 함수로는 execl(const char * path, const char * arg, arg2,...); 혹은 execlp(const char*file, const char * arg ...);이다. 전자는 경로(절대 or 상대)를 직접 입력하여 실행하고, 후자는 환경 변수PATH에 포함된 경로에서 파일을 검색해서 실행한다. (없을시 에러) 주의할 점은 둘 다 인자의 마지막엔 NULL을 넣어서 끝내줘야한다는 것이다. 실패시에 -1을 리턴하며 성공시엔 반환 없이 바로 파일이 실행되고, 뒤에 있던 코드는 새 프로세스로 싸그리 대체되어 실행되지 않는다.
fork()
현재 실행중인 프로세스를 복제하여 자식 프로세스를 만든다. 부모 프로세서에서는 fork의 반환값으로 자식 프로세스의 PID를 리턴 받고 자식 프로세스는 0을 리턴받는다. 실패시 자식 프로세스는 생성되지 않고 -1을 반환한다. 부모는 명령어 실행 후 다음 줄로 넘어가고, 자식 프로세스는 정확히 부모와 같은 구조를 가진 별개의 프로세스이지만 fork()가 호출된 이후 줄부터 실행된다.(처음줄부터 실행되지 않는다). 보통은 리턴 받은 값을 이용해서 부모와 자식의 역할을 나눠서 수행하도록 코드를 작성한다. 만약 자식 프로세스가 끝날때까지 부모가 기다려야하는 경우면 부모 프로세스에 wait()함수를 이용해서 발전시킬 수 있다.
wait(int * status)
자식 프로세스 중 아무나 하나 종료되길 기다린다. 리턴값은 종료된 자식의 PID이며, status에 종료 사유가 저장된다.
waitpid(pid_t pid, int *status, int options); 의 경우 특정 PID를 가진 자식 프로세스의 종료를 기다린다는 뜻이다 pid는 -1이라면 wait과 완전 동일하고, 그보다 작은 값을 가지면 절대값에 해당하는 PID를 기다린다. options 에 WNOHANG을 입력하면 종료를 기다리지 않고 리턴하므로 실행 여부를 확인할 때 쓴다.
좀비(Z)상태와 고아
리눅스에서는 부모 프로세스가 자식 프로세스의 종료를 인지한다(SIGCHLD신호받아서). 자식 프로세스가 종료될 경우 운영체제에 알리고 프로세스를 완전히 종료하는 과정이 필요한데, 부모가 역할을 해주지 않고 종료되는 경우가 있다. 이 상태를 죽었지만 죽지 않고 남아있는 상태인 좀비 상태라고 부른다. 이는 프로세스에선 더 이상 접근하여 직접 종료시킬 수 없으므로, shell 에서 kill로 죽여줘야한다. 좀비 상태인 프로세스가 많아질수록 시스템엔 부담이 되니 남아있지 않도록 프로그래밍해야 한다. 자식의 종료를 처리하기 위해선 부모 프로세스에서 wait()함수를 호출해주면 된다.
또한 부모가 모종의 이유로 자식 프로세스보다 먼저 종료될 경우 자식은 고아 프로세스가 되어 종료를 알릴 수 없게 된다. 이런 프로세스 또한 계속해서 실행중인 것처럼 인식되기 때문에 성능 저하를 야기할 수 있으므로 신중하게 설계해야한다.
signal(int signum, sighandler_t handler(사용자정의함수))
앞서 return status 에서 뒤 8비트가 signal number라고 했었다. 시그널들은 프로세스에게 특정 신호를 전달하여 동작을 제어할 수 있게 도와주는 고마운 녀석이다. 주로 사용하는 ctrl+c는 SIGINT(2) 를 보내고, ctrl+w는 SIGQUIT(3)을 보낸다.
물론 사용자가 임의로 시그널에 따른 행동을 제어할 수도 있는데, 그게 signal함수이다. handler에 (sighandler_t) -1 일 경우 에러를 반환하고, 0일 경우 default로 설정된 행동, 1일 경우 signal을 무시한다.
void sigint_handler(int signal){} 로 특정 시그널이 들어왔을 때의 동작을 정의하며
sig = signal(SIGQUIT, sigint_handler)로 소환하여 SIGQUIT이 들어왔을 때 어떤 함수를 호출할 것인지를 정해준다. 인자는 SIGQUIT으로 자동으로 전달된다.
kill(pid_t pid, int sig);
쉘이 아닌 프로세스가 시그널을 보낼 수 있다. 특정 pid에 보내면 되고, (-1일 경우 허용된 모든 프로세스에게) signal number를 보내면 된다. kill 을 보낼 경우 쉘에서의 kill과 동일한 결과를 얻을 수 있다.
alarm(unsigned int sec);
특정 sec초 후에 SIGALRM을 자기 자신에게 호출한다. sec=0 이면 알람을 끄고, 시그널 발생 전에 다시 선언할 경우 시간이 갱신된다.
int pause(); 와 함께 사용할 경우, 프로세스를 sleep상태로 전환하며 signal을 기다리므로 알람이 울릴 때까지 쉴 수 있다.
프로세스간 통신
1. 파이프
부모 자식간의 통신을 위해서 이용하는 파이프라인이다. 프로세스마다 갖고 있는 파일 식별자 테이블을 이용하여 서로 통신을 한다. 파일 식별자를 관리하기 위해 테이블을 사용하는데, 0은 stdin, 1은 stdout, 2는 stderr로 배정되어 있다. fork()를 사용하여 자식 프로세스를 만들었다면 파일 식별자 테이블마저 동일하므로 서로 입출력을 맞물리게 해준다면 통신이 가능할 것이라 예측할 수 있다.
파이프의 구현은 다음 단계를 따른다.
가. pipe()함수를 통해 파이프를 생성한다.
나. 각 파이프의 끝을 연결핟나.
pipe(pipefd[2]) 는 pipefd 배열 0번에 읽을 식별자를, 1번에 쓸 식별자의 번호를 저장한다. 그리고 write() 등의 함수로 데이터를 pipefd[1]을 통해 전송하면, 자식 프로세스에서는 파이프 정보도 동일하므로 pipefd[0] 에서 읽어오면 된다. 반대로 자식에서 pipefd[1]에 쓴다면 부모에서 pipefd[0] 으로 받아도 된다. 물론 stdin stdout을 설정하면 표준 입출력처럼 쓰고 읽을 수도 있다.
이 때 close()와 dup()를 이용하여 쓰던 곳을 닫거나(이용하려는 fdt 변경 혹은 입력만 가능하게 만듦) 파일 식별자를 복사해 파일 식별자의 번호를 바꿀 수 있다.
파이프는 간단하게 만들어 쓸 수 있다는 장점이 있지만, 읽고 쓰는 타이밍을 잘 맞춰야 전송에 문제가 없으며 (운이 나쁘면 자기가 자기꺼 받아서 씀) 부모-자식이 아닌 프로세스는 통신할 수 없다는 단점이 있다.
2. FIFO(파일 입출력)
파이프의 단점을 개선하기 위해 이름있는 파이프를 만든다. 결국 파이프와 비슷하지만 FIFO용 파일을 통해 통신을 하기 때문에 부모-자식이 아니어도 통신이 가능하다. fifo 는 mkfifo(const char * path, int mode)로 파일을 생성하여 접근 권한을 설정한뒤 통신에 이용하며, 일반적인 파일이 아니고 파이프를 위한 파일이므로 파이프로 쓸 때만 쓴다. 물론 이럴 경우 파일에는 한 번에 하나만 접근할 수 있어야 의도치 않은 결과를 막을 수 있고, 오류도 막을 수 있다. 이 때 필요한 개념이 critical section이다. 공통적으로 겹치면서 오류를 유발할 수 있는 공간을 의미하는데, 가장 간단한 세마포어부터 살펴본다.
세마포어는 일종의 flag를 이용한 방식이다. 임계영역에 접근하고 있는 프로세스가 있다면 숫자를 증가시켜 일정 갯수가 넘으면 접근을 차단하고 대기시킨다. 물론 이런 방식을 지원하기 위한 함수가 있다.
semget(key, n, flag) 를 사용하면 세마포어 배열을 할당할 수 있다. 배열로 할당해주기 때문에 여러 임계 영역에 대해 한 번에 설정이 가능하다. 이런 배열을 다루기 위해 semctl(sem_id, idx, command, param) 함수를 사용하며 param과 command를 이용해 세마포어 값을 설정하거나 배열을 제거하는 등의 일이 가능하다.
semop()를 통해 세마포어를 작동시킬 수 있고, 인자로 주는 buf를 통해 동작을 전달한다.
예시 )
union sb; sb.sem_num =0;
sb.sem_op=-1; sb.sem_flg=SEM_UNDO; //설정
ret = semop(id_sem, &sb, 1) ; //1개의 union sb 를 sem id에 전달하여 세마포어 작동. 임계영역 진입
//... 기능 구현
sb.sem_num =0; sb.sem_op=1; sb.sem_flg = SEM_UNDO;
ret= semop(id_sem, &sb, 1); //임계영역 탈출
3. 공유 메모리 (Shared Memory)
세마포어를 이용하는 또 다른 방식은 공유되는 메모리 공간을 잡아서 사용하는 방법이다.
파이프는 부모-자식간에만 사용할 수 있었고, File 로 파이프를 만들어 사용하는 방식인 FIFO는 파일을 통해 파이프의 문제를 해결하지만 결국 송신-수신의 관계를 위한 방식이었다.
공유 메모리는 직접 통신을 위한 방식이라기보다 공통 영역을 설정해놓고 거기서 변수를 설정하거나 가져다 쓰는 방식인 것 같다.
프로세스 별로 자신의 주소 공간에 공유 메모리용 세그먼트를 분리하고, 자신의 세그먼트에 접근하여 실제 물리적으로 공유되는 메모리에 접근하는 방식을 사용한다.
사용 방법은 두 단계를 따른다.
1. 공유 메모리를 생성한다.
2. 공유 메모리를 프로세스에 붙인다.
프로세스 별로 직접 공유 메모리를 붙여줘야하므로 주의를 기울이자.
shmget(key, size, flag);
공유 메모리를 할당한다. key는 할당받은 공유 메모리에 접근하기 위한 key값이며 사용자가 정해서 쓰면 된다. size는 메모리 크기고 flag는 공유 메모리 할당에 관련된 flag들이고 찾아서 쓰면 된다. 반환 값은 공유 메모리의 ID이다.
shmat(id, *addr, flag);
프로세스에 공유 메모리를 붙인다. 여기서 id는 shmget 에서 리턴 받은 id를 쓰면 되고, addr은 직접 공유 메모리를 부착할 주소를 전해주거나 NULL로 비어있는 곳에 붙이면 된다. flag는 공유 메모리의 W/R을 설정할 수 있다. 공유 메모리의 주소가 반환된다.
shmdt(*addr);
프로세스의 주소 공간과 공유 메모리를 분리시킨다. shmat의 반환 값을 addr에 넣어서 떼면 된다. 성공시 0을 리턴한다.
shmctl(id, cmd, *buf);
공유 메모리를 실제적으로 제어할 때 사용한다. 공유 메모리의 아이디와 명령을 이용하여 제어하고, buf에 리턴을 주로 받아온다.
공유 메모리의 정보를 가져오려면
ret = shmctl(id_shmem, IPC_STAT, &buf); 로 가져오고 buf를 확인하면 된다.
공유 메모리를 완전히 제거하려면
ret = shmctl(id_shmem, IPC_RMID, 0); 을 하면 된다.
공유 메모리에 뭔가를 쓰고 싶다면 sprintf(shmem->buf, "%d %d", id,*adrr) ; 등을 이용하면 된다.
공유 메모리 또한 값을 제대로 쓰고 있는지에 대한 확인이 필요하다.
간단한 방식으로, buf에 채워지는 값들의 ascii 값을 단순히 합하여 입력값과 저장된 값을 비교하여 바로바로 확인하여 다르면 저장하지 않는 식으로 안전성을 확보할 수도 있다.
또한 두 곳에서 동시에 쓰면 의도치 않은 문제가 발생할 수 있기 때문에 동시에 접근하여 읽는 것은 허용하되, 쓰는 중에 가져가거나 동시에 쓰는 것을 허용하지 않는다.
4. 메세지 큐(Message Queue)
위의 방식들은 직관적이며 편리하다.
하지만 조금 더 개선할 수 있다.
파이프의 방식은 좋지만 번거로움이 존재하고, 공유 메모리 또한 선언, 부착, 접근, 제어의 방식이 번거롭게 느껴질 수 있다.
메세지 큐를 이용하면 따로 구현하지 않고도 자신이 받을 데이터와 보낼 데이터를 확실하게 구분할 수 있고, 따로 준비가 필요하지도 않아서 프로세스간 통신을 쉽게 이용할 수 있다. 물론 안정성을 위해 설정할만한 것들은 있다. 또한 최대 8kB 까지만 가능하므로 큰 파일이나 buf 는 전달할 수 없다.
msgget(key, flag);
메세지 큐를 만든다. key값은 사용자가 정하여 같은 메세지 큐를 이용할 프로세스에서 동일하게 사용해주면 된다. flag에는 메세지 큐 생성시의 옵션들을 추가할 수 있다. 0666|IPC_CREAT 를 추가한다면 권한을 666으로 설정하고, 이미 존재한다면 만들지 않는다. 성공시 메세지 큐의 ID를 반환한다.
msgctl(id, cmd, *buf);
메세지 큐를 연결했다면 id가 나오는데, 그 곳에 명령과 buf를 전달하여 메세지 큐를 제어한다.
msgsnd(id, *msg, size, flag);
메세지 큐에 메세지를 보내는 함수. *msg는 메세지의 buf를 직접 만들어서 메세지의 타입과 메세지를 설정해주면 되며 struct msg_buf를 이용하여 그 주소를 전달해주면 된다. size는 메세지 타입 4B를 제외한 buf의 크기를 입력해주면 되고 flag는 옵션을 설정한다. 성공시 0을 반환한다.
기본적으로 메세지 타입(mtype)은 항상 필수로 존재하며 0보다 큰 값을 가져야 한다. 만약 메세지 큐가 꽉 찼다면 공간이 생길 때까지 잠들어버리므로, 공간이 없을 때 바로 종료해버리고 싶다면 flag에 IPC_NOWAIT을 사용하면 된다.
msgrcv(id, *msg, size, msg_type, flag);
메세지를 받는다. 보낸 메세지와 동일한 구조체를 줘야하며, size는 마찬가지로 mtype(4B)를 제외한 크기를 입력해줘야한다. msg_type이 조금 중요한데, 0이면 가장 먼저 들어온걸 읽고, >0이면 mtype과 같은 메세지 중 가장 먼저 들어온 걸 읽는다. 즉 mtype=1,2 로 두 가지를 넣었고, msg_type = 2로 받았다면 mtype=1 보다 mtype =2 가 먼저 읽힌다.
<0 이면 msg_type의 절대값보다 작거나 같은 메세지 중 가장 작은 mtype의 메세지를 읽어온다. 즉 앞선 상황에서 msg_type = -2라면 마찬가지로 mtype=1가 먼저 읽힌다.
예시)
struct msg_buf{
long mtype;
char buf[64];
}msgbuf = {1, "HI"};
//메세지 송신 프로세스
id_msg = msgget( KEY, 0666|IPC_CREAT);
ret = msgsnd(id_msg, &msgbuf, sizeof(struct msg_buf) - sizeof(long), 0);
//메세지 수신 프로세스
id_msg = msgget(KEY, 0666|IPC_CREAT);
ret = msgrcv(id_msg, &msgbuf, sizeof(struct msg_buf) - sizeof(long), msg_type, IPC_NOWAIT);
송수신한 데이터는 msgbuf에서 확인하면 된다.
쓰레드(Thread)
프로세스 내의 프로세스 같은 개념인 쓰레드다.
리눅스는 POSIX(Portable Operating System Interface) Thread를 사용하는데, UNIX OS 간 공통 API를 사용하기 위해 선택한 규격이다. 윈도우에서도 지원한다.
기본적으로 프로세스가 실행되면 기본 쓰레드가 실행되며, 추가적으로 사용자가 생성하여 쓸 수 있다. 쓰레드는 프로세스 내의 자원을 공유하지만 개별 스택과 프로그램 카운터(?)를 갖고 실행되므로 독립적인 실행이 가능하다.
커널은 쓰레드(테스크 Task) 단위로 관리, 실행되므로 쓰레드 별로 역할을 지정할 수 있다.
물론 같은 프로세스내의 자원을 공유하므로 같은 메모리에 접근하는 등의 상황에서 세마포어 등 동기화 방법을 잘 설정해야한다.
pthread_create(*thread, *attr, *(*routine)(), *arg);
쓰레드를 생성한다. *thread는 pthread_t 구조체를 선언하여 그 주소를 넘겨주면 되고, *attr은 쓰레드의 속성을 결정하는데, 기본값은 NULL로 설정된다. routine이 좀 중요한데, 쓰레드의 시작함수를 설정하는 것이고, 이 시작함수가 사실 쓰레드의 메인 함수가 된다. arg는 인자로 전달할 것이 필요할 때 쓰면 된다.
일단 쓰레드가 생성될 경우 쓰레드가 종료될 경우는 다음과 같다. exit()가 호출되거나, 함수가 return을 만나 종료되거나, 메인 쓰레드에서 cancel()을 이용해 죽일 경우, 메인 쓰레드가 종료될 경우.
pthread_exit(*ret);
해당 쓰레드를 종료한다. 리턴은 없으며 *ret는 메인 쓰레드에 전달할 데이터를 넣어주면 된다. 없으면 NULL을 넣는다.
pthread_join(id, **ret);
해당 id를 가진 쓰레드의 종료를 기다린다. **ret 는 pthread_exit(*ret)에서 전달한 값을 받아온다. 필요 없으면 NULL을 넣는다.
예시)
int *routine(void *arg){
printf("%s is received\n", (char *) arg);
for (int i=0; i<10; i++){ printf("[%d] Hello!\n", i);
return 0; //pthread_exit("THREAD ENDED");
}
int main(){
pthread_t id; void * ret;
ret = pthread_create(&id, NULL, routine, "START THREAD");
ret = pthread_join(id, &ret);
//pthread_exit("THREAD ENDED");
//printf("RECEIVED : %s\n" ,(char *) ret);
return EXIT_SUCCESS;
}
단순히 구현할 경우 데이터에 동시에 접근할 때 문제가 발생할 수 있다.
기본적으로 ptrhead에서 지원하는 세마포어를 이용한다.
뮤텍스(MUTEX, Mutual Exclusive Binary Semaphore)
세마포어와 동일하게 작동하며, critical section(임계영역)을 구현하여 한 번에 하나의 쓰레드만 접근할 수 있는 공간을 만든다. 다른 쓰레드는 sleep상태로 대기한다.
pthread_mutex_init(*mutex, *attr);
뮤텍스를 초기화한다. pthread_mutext_t mutex; 구조체를 선언해주고 *mutex에 주소를 넣어준다. attr은 NULL로 주로 사용한다. 성공시 0을 반환한다. 후에 사용은 mutex를 이용해서 한다.
pthread_mutex_lock(*mutex);
임계영역에 들어설 때 lock시키면서 들어간다. lock 상태(0)에선 다른 쓰레드가 이용할 수 없다.
pthread_mutex_unlock(*mutex);
임계영역에서 나올 때 unlock 시킨다.(1)
pthread_mutex_destroy(*mutex);
만들어진 뮤텍스를 제거한다.
예시)
pthread_mutex_t mutex;
void *routine1(){
for (int i=0; i<10; i++){
pthread_mutex_lock(&mutex);
sleep(10); printf("routine 1 : [%d]\n", i);
pthread_mutex_unlock(&mutex);
}
}
void *routine2(){
for (int i=0; i<10; i++){
pthread_mutex_lock(&mutex);
sleep(10); printf("routine 2 : [%d]\n", i);
pthread_mutex_unlock(&mutex);
}
}
int main(){
pthread_t thread1, thread2;
ret = pthread_mutex_init(&mutex, NULL);
ret = pthread_create(&thread1, NULL, routine1);
ret = pthread_create(&thread2, NULL, routine2);
ret = pthread_join(thread1, NULL);
ret = pthread_join(thread2, NULL);
pthread_mutex_destroy(&mutex);
return EXIT_SUCCESS;
}
POSIX 세마포어
이름이 없는 세마포어를 만들어서 사용할 수도 있다.
sem_init(*sem, shared_type, value);
세마포어를 초기화 및 생성한다. *sem에는 내가 선언한 구조체의 주소가 들어가며, shared_type 이 0이면 쓰레드간 사용, 1이면 프로세스간 사용을 의미한다. value는 세마포어가 가질 초기값을 넣어준다. 성공시 0을 반환한다.
sem_wait(*sem);
임계 영역에 진입할 때 세마포어의 값을 1 감소시킨다. 0이 될 때까지 다른 쓰레드나 프로세스가 진입할 수 있다. 성공시 0을 반환한다. 0이라면 sleep상태로 대기한다.
sem_post(*sem);
임계 영역에서 탈출할 때 세마포어의 값을 1 증가시킨다. 성공시 0을 반환.
sem_destroy(*sem);
세마포어를 파.괴한다.
이렇게 리눅스 쉘과 쉘 스크립트의 기본 사용법에 대해 기록해두었다.
오랜만에 보니 잘 기억이 나지 않는 것들도 있는데, 쓸 때마다 찾아서 보완해놔야겠다.
프로세스와 쓰레드에 대해서도 기록했는데, 이게 또 제대로된 이해를 바탕으로 한게 아니고
이런 식으로 쓰면 된다 식으로 넘어갔기 때문에 중간중간 많이 이해가 비는 것이 느껴진다.
만약 리눅스를 더 사용하게 된다면 이걸 확실히 짚고 넘어가야할 것 같다.