SED(Stream Editor)

SED는 Stream Editor의 약자로 sed라는 명령어로 원본 텍스트 파일을 편집하는 유용한 명령어입니다. 리눅스의 editor라하면 생각나는 에디터가 있지 않나요? vi 편집기가 대표적인데, 여러분들이 vi편집기로 편집할때는 vi filename의 명령을 이용해서 파일을 열고 난 이후에 각종 vi 명령어를 통해서 이리 저리 움직여 추가, 삭제, 변경 등의 편집을 하게 되죠. 그리고 작업이 다 끝나게 되면 저장 후 나가게 됩니다. 이때 여러분은 원래의 파일을 변경하여 저장하기 때문에 원본은 변경됩니다. 그리고 vi는 실시간 저장할 수가 있습니다. 여기서 sed는 vi 편집기와는 다르게 이런 차이점이 있습니다.

 

1. sed와 vi가 다른 점은 sed는 명령어 형태로 편집이 되며 vi처럼 실시간 편집이 아닙니다.

2. 원본을 건드리지 않고 편집하기 때문에 작업이 완료되었어도 기본적으로 원본에는 전혀 영향이 없다는 점입니다.(단, 여러분이 sed옵션에서 -i 옵션을 지정한다면 원본을 바꾸게 됩니다.) 그래서 내부적으로 특수한 저장 공간인 버퍼를 사용합니다. 두 가지 버퍼는 패턴 버퍼(패턴 스페이스라고도 합니다)와 홀드 버퍼(홀드 스페이스라고도 합니다)입니다.

 

아래의 그림을 보면 sed는 InputStream으로 파일의 내용을 가져옵니다. 그리고 난 후에 패턴 버퍼에 그 내용을 담고 있으며 데이터의 변형과 추가를 위해 다시 임시 버퍼를 사용하게 되는데, 그게 홀드 버퍼입니다. 그리고 작업이 전부 완료되었다면 패턴 버퍼에 그 내용이 담기는데, 그 내용을 OutputStream으로 보내주게 되면 비로소 우리가 원하는 결과가 출력되는겁니다. 기본적으로 OutputStream은 콜솔화면인 stdout입니다. 우리는 그냥 sed가 내부적으로 2개의 버퍼를 가지고 있다고 보시면 됩니다. 

pattern space and hold space

 

 

이제 sed의 기본적인 개념을 알아보았으니 사용법에 대해서 알아보아야겠죠? 그 전에 sed에 대한 input 파일은 아래의 내용처럼 간단한 텍스트 파일로 하도록 하겠습니다. 필요에 따라 다른 text파일도 사용하도록 하겠습니다.

 

sed_test_file.txt

name    phone           birth           sex     score
reakwon 010-1234-1234   1981-01-01      M       100
sim     010-4321-4321   1999-09-09      F       88
nara    010-1010-2020   1993-12-12      M       20
yut     010-2323-2323   1988-10-10      F       59
kim     010-1234-4321   1977-07-17      M       69
nam     010-4321-7890   1996-06-20      M       75
sol     010-5911-1111   1976-10-12      F       60
lee     010-4949-4949   1988-09-30      F       80
feng    010-1111-9999   1979-03-20      M       90

 

기본적으로 sed는 아래와 같은 형태로 사용합니다. 

sed -n -e 'command' [input file]

 

 

-n : sed는 pattern buffer의 내용을 자동적으로 출력해주는데, 이 옵션을 사용하게 되면 자동 출력을 하지 않습니다. -n옵션을 하지 않고 sed명령을 해보시면 눈에 띄게 많은 데이터가 출력되는것을 확인할 수 있습니다. 매우 지저분하지요. 그래서 -n옵션을 붙여 패턴 버퍼의 자동출력을 하지 않습니다. 여러분은 그래서 sed 명령의 기본 형태는 sed -n으로 생각하시면 됩니다.

 

-e : 이 옵션 다음으로는 우리가 사용할 command를 가지고 텍스트 파일을 가공해줍니다. -e의 command에는 어떤 종류가 있는지 확인해보도록 합시다.

그리고 맨 마지막에는 우리가 가공할 원본 파일을 지정해주면 됩니다. 

1. 특정 행을 출력 - p (print)

1-1. 천재 내용을 출력 : 기본적으로 전체의 줄을 출력하려면 command에 /$/p 또는 1,$p로 출력해볼 수 있습니다. 

sed -n -e '1,$p' sed_test_file.txt
sed -n -e '/$/p' sed_test_file.txt

 

1-2. 첫번째 줄 출력 : 여러분이 첫번째 줄만 출력해주기를 아래의 command를 사용할 수 있습니다. 

 sed -n -e '1p' ./sed_test_file.txt
name    phone           birth           sex     score

 

pprint의 p를 의미합니다. 

1-3. start ~ end 줄까지 출력 : 그렇다면 첫째줄부터 4번째줄을 출력하기를 원한다면 쉼표로 구분된 출력하길 원하는 줄, 마지막 줄 p 의 형태를 사용하면 됩니다.

sed -n -e '1,4p' sed_test_file.txt
name    phone           birth           sex     score
reakwon 010-1234-1234   1981-01-01      M       100
sim     010-4321-4321   1999-09-09      F       88
nara    010-1010-2020   1993-12-12      M       20

 

1-4. 특정줄에서 마지막 줄까지 출력 : 어떤 줄에서 마지막 줄까지 출력하려면 $문자를 사용하면 됩니다. $는 마지막줄을 의미합니다. 그래서 5번째 줄에서 마지막줄까지 출력하려면 아래와 같은 command를 사용할 수 있습니다.

sed -n -e '7,$p' sed_test_file.txt
nam     010-4321-7890   1996-06-20      M       75
sol     010-5911-1111   1976-10-12      F       60
lee     010-4949-4949   1988-09-30      F       80
feng    010-1111-9999   1979-03-20      M       90

 

1-5. 다중 command 사용 : 만약 command를 여러개 사용하고 싶다면 -e 옵션을 이용해서 여러개 사용하여 command를 줄 수 있습니다. 만약 1번째 줄을 출력해주고, 8번째 줄부터 끝까지 출력하기를 원한다면 아래의 명령을 사용하면 됩니다.

sed -n -e '1p' -e '8,$p'  sed_test_file.txt
name    phone           birth           sex     score
sol     010-5911-1111   1976-10-12      F       60
lee     010-4949-4949   1988-09-30      F       80
feng    010-1111-9999   1979-03-20      M       90

 

1-6. 특정 문자열이 있는 줄 출력

만약 위의 파일에서 여자(F, Female)만 출력한다고 하다면 어떻게 command를 어떻게 사용할까요? 아래와 같은 형식대로 사용하면 됩니다.

/포함된 문자열/p

 

텍스트 파일에서 우리가 찾을 문자열이 포함된 줄을 출력(p, print)한다고 생각하시면 됩니다. 아래의 명령이 여성만 뽑아내는 command입니다.

sed -n -e '/F/p'  sed_test_file.txt

 

2. 특정 행 삭제 - d (delete)

행삭제에 관한 명령어d를 사용하면 됩니다. d를 사용하는 것외에는 위의 줄 출력을 해주는 명령어 형태로 사용하면 됩니다. 

2-1. 2~6번째 줄을 삭제하고 나머지 모든 내용을 출력

sed -n -e '2,6d' -e '1,$p'  sed_test_file.txt
name    phone           birth           sex     score
nam     010-4321-7890   1996-06-20      M       75
sol     010-5911-1111   1976-10-12      F       60
lee     010-4949-4949   1988-09-30      F       80
feng    010-1111-9999   1979-03-20      M       90

 

3. 단어 치환(Substitute) - s (substitute)

이제부터는 출력결과는 생략하도록 하겠습니다. 특정 단어를 치환하려면 s명령을 이용하면 됩니다. 형식은 아래와 같습니다. 

3.1 기본적인 특정 단어 치환

s/old/new/g
s/old/new/gi

 

단어 s substitute(치환)의 약자, 그리고 /로 구분하여 old는 단어를 치환할 문자열, new는 새롭게 치환한 문자열인데 비어있으면 그 문자열을 삭제한 효과를 가질 수 있습니다. gglobal의 약자로 전체에 적용됨을 의미합니다. 

두번째의 i는 ignore case의 약자로 old의 단어를 검색할때 대소문자 구분하지 않겠다는 것을 의미합니다. 

아래 명령의 결과를 보고 차이점을 확인해보세요.

sed -n -e 's/reakwon/reak/g' -e '2p' sed_test_file.txt
sed -n -e 's/reakwoN/reak/g' -e '2p' sed_test_file.txt
sed -n -e 's/reakwoN/reak/gi' -e '2p' sed_test_file.txt

 

3.2 특정 단어로 시작 혹은 끝나는 단어를 포함하는 라인의 문자열 치환

특정단어로 시작하는 단어로 문자열을 치환하는 것도 가능합니다. 특정 단어로 시작하는 줄을 선택하려면 앞에 '^'문자를 사용하면 되죠. 사실 정규표현식을 아신다면 앞에 ^가 붙는다면 ^이후에 나오는 문자열로 시작되는 문자열들을 추출한다는 것을 아실겁니다. 사실 몰라도 외워서 아시면 됩니다. 단 많이 실수하시는 부분이 특정 단어로 시작하거나 끝나는 문자열이 아닌 줄(line)입니다.

 

아래의 파일(let_it_go.txt)에서 Let으로 시작하는 줄의 첫 Let를 LET으로 바꿔보려면 다음 보이는 명령을 사용하면 됩니다.

Let it go, let it go.
Can't hold it back anymore.
Let it go, let it go.
Turn away and slam the door.
I don't care what they're going to say.
Let the storm rage on.
The cold never bothered me anyway.

 

sed -n -e 's/^let/LET/gi' -e '1,$p' let_it_go.txt

 

반대로 끝나는 문자열은 끝에 $를 붙여줘서 검색하면 됩니다. Anyway. 으로 끝나는 줄의 Anyway를 대문자로 바꾸려면 아래와 같은 command를 사용하면 됩니다.

sed -n -e 's/anyway.$/ANYWAY/gi' -e '1,$p' let_it_go.txt

 

4. 문자열 추가 - a, i (append, insert)

문자열을 추가하는 방법에는 두 가지 정도가 존재합니다. 해당 문자열 아래에 추가하느냐(Append) 아니면 이 전 줄에 삽입하느냐(Insert)가 있는데요. 기본적인 형식은 아래의 command처럼 사용합니다.

/찾을 문자열/a\다음 출에 추가할 문자열
/찾을 문자열/i\위에 삽입할 문자열

 

찾을 문자열 뒤에 추가할 문자열을 위의 형식대로 사용하면 됩니다. 위의 let_it_go.txt파일에서 bye로 끝나는 줄 다음에 end라는 줄을 넣고 싶다면 이렇게 사용하면 됩니다. 반대로 위에 추가하려면 a를 i로 바꾸면 되겠죠.

 

 

sed -n -e '/bye/a\end' -e '1,$p' let_it_go.txt

 

5. 특정 행의 내용을 전부 교체 - c (change)

c command를 이용해서 행의 내용을 바꿀 수 있습니다. command 형태는 이렇습니다.

/바꿀 행이 포함한 문자열/c\바꿀 행의 내용

 

예를 들어 Let으로 시작하는 줄의 내용을 바꾸고 싶다면 어떻게 할까요? 우선 ^를 사용하여 Let으로 시작하는 줄들을 찾고 c 커맨드로 바꿔질 줄 내용을 입력해주시면 됩니다.

sed -n -e '/^Let/c\Let it go X2' -e '1,$p' let_it_go.txt

 

6. 특정 행에 파일의 내용을 추가 - r (read)

혹은 파일의 내용을 줄에다가 추가할 수도 있습니다. 여기 우리가 내용을 추가할 파일이 존재합니다.

 

perfect.txt

PERFECT! EXCELLENT!

 

100으로 끝나는 줄에 저 텍스트 파일의 내용을 아랫줄에 첨가하려면 아래와 같은 명령을 사용하면 됩니다.

 sed -n -e '/100$/r perfect.txt' -e '1,$p' sed_test_file.txt
name    phone           birth           sex     score
reakwon 010-1234-1234   1981-01-01      M       100
PERFECT! EXCELLENT!
sim     010-4321-4321   1999-09-09      F       88
nara    010-1010-2020   1993-12-12      M       20
yut     010-2323-2323   1988-10-10      F       59
kim     010-1234-4321   1977-07-17      M       69
nam     010-4321-7890   1996-06-20      M       75
sol     010-5911-1111   1976-10-12      F       60
lee     010-4949-4949   1988-09-30      F       80
feng    010-1111-9999   1979-03-20      M       90

 

이상으로 sed에 대한 기본 개념과 사용방법, 그리고 예제를 살펴보았습니다. 여러분이 정규표현식을 사용하여 깊게 사용할 수도 있지만, 저는 정규표현식을 거의 사용할 일이 없으니 여기까지 하도록 하겠습니다. 이정도만 알아도 sed 명령을 사용하는 것에는 불편함이 없을 겁니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

더 많은 정보를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

AWK(Aho Weinberger Kernighan)

리눅스의 어떤 다른 명령어보다 명령어 이름이 매우 직관적이지 않은 명령어입니다. 이 명령어를 개발한 사람들의 이름 약자(Aho  Weinberger Kernighan)이기 때문에 mkdir(make directory), rm(remove) 같은 의미를 축약하여 만든 명령어와는 명령어 이름이 좀 그렇습니다. 이 명령어를 읽을때는 주로 오크라고 읽습니다. AWK는 유닉스에서 개발된 스크립트 언어로 텍스트가 저장되어 있는 파일을 원하는 대로 필터링하거나 추가해주거나 기타 가공을 통해서 나온 결과를 행과 열로 출력해주는 프로그램입니다. 엄청나게 막강하고 다양한 기능을 담고 있기 때문에 여기서는 어떻게 사용하는지에 대해서만 알아보도록 하겠습니다.

그 전에 간단하게 기본 용어만 짚고 넘어가도록 합시다. 아래는 하나의 텍스트 파일에 기록된 내용을 보여주고 있습니다. 여기서 각 단어들은 공백으로 구분되어 집니다. 각 줄(line)은 레코드(Record)라고 칭합니다. 그리고 그 안에 각각의 단어들이 필드(Field)가 되겠습니다.

 

AWK에서는 레코드가 $0, 그리고 $1, ..., $N 은 필드를 나타내는 열을 나타냅니다. 우리들이 사용할 파일은 위 내용은 아니고 아래의 내용을 담고 있습니다. 

 

file : awk_test_file.txt

name    phone           birth           sex     score
reakwon 010-1234-1234   1981-01-01      M       100
sim     010-4321-4321   1999-09-09      F       88
nara    010-1010-2020   1993-12-12      M       20
yut     010-2323-2323   1988-10-10      F       59
kim     010-1234-4321   1977-07-17      M       69
nam     010-4321-7890   1996-06-20      M       75

 

awk의 기본 사용법은 패턴(pattern)액션(action)을 정의하여 입력으로 주어진 파일의 데이터를 가공하여 출력합니다. 이제부터 예를 볼 겁니다. 패턴과 액션 중 하나만 사용하여도 무관합니다.

awk pattern {action} 

 

1. 열(Column)만 출력하기

각 $1, $2, $3 ... 은 열에 대응한다고 했었죠. 그리고 $0는 레코드에 대응한다고 했습니다. 여기서 이름만 모두 출력하겠다고 한다면 아래와 같이 awk 명령을 수행하면 됩니다.

awk '{ print $1 }' ./awk_test_file.txt
name
reakwon
sim
nara
yut
kim
nam

 

여러개의 열도 출력 가능합니다.

 awk '{ print $1,$2 }' ./awk_test_file.txt
name phone
reakwon 010-1234-1234
sim 010-4321-4321
nara 010-1010-2020
yut 010-2323-2323
kim 010-1234-4321
nam 010-4321-7890

 

여기서 awk의 기본적인 action은 print이며 모든 열을 전부 출력합니다.

2. 특정 pattern이 포함된 레코드 출력

다음의 awk 명령은 rea라는 문자열이 포함된 레코드를 출력해주는 명령입니다. 

 awk '/rea/' ./awk_test_file.txt
reakwon 010-1234-1234   1981-01-01      M       100

 

3. 출력의 내용 첨가

awk는 print에 문자열을 추가하여 출력물의 내용에 문자열을 추가할 수 있습니다. 만약 이름을 명시적으로 나타내기 위해 "name : " , 그리고 휴대폰 번호를 명시적으로 나타내려고 "phone : " 를 추가해서 출력하고 싶다면 아래의 awk 명령을 사용하면 됩니다.

awk '{ print ("name : " $1, ", "  "phone : " $2) }' ./awk_test_file.txt

아래 출력에서 맨 윗줄은 무시하시면 됩니다.

name : name , phone : phone
name : reakwon , phone : 010-1234-1234
name : sim , phone : 010-4321-4321
name : nara , phone : 010-1010-2020
name : yut , phone : 010-2323-2323
name : kim , phone : 010-1234-4321
name : nam , phone : 010-4321-7890

 

4. 특정 Record를 검색하기 - if 구문

4-1. ~이상 , ~ 이하의 레코드 출력

만약 위의 파일에서 점수가 80점 이상인 사람들의 레코드를 알고 싶다면 어떻게 하면 좋을까요? 이거는 pattern을 써야할까요, action을 써야할까요? action에서는 if 구문이 존재합니다. 그래서 이렇게 사용하면 80점 이상인 record를 출력할 수 있습니다.

awk '{ if ( $5 >= 80 ) print ($0) }' ./awk_test_file.txt
name    phone           birth           sex     score
reakwon 010-1234-1234   1981-01-01      M       100
sim     010-4321-4321   1999-09-09      F       88

 

혹은 아래와 같은 구문으로도 사용할 수도 있습니다.

awk '$5 >= 80 { print $0 }' ./awk_test_file.txt

 

4-2. 남자의 데이터만 출력하기

역시 if 구문으로 지정할 수 있는데 단어 자체가 같은 것을 비교하려면 쌍따옴표(")해주면 됩니다. 

awk '{ if ( $4 == "M" ) print ($0) }' ./awk_test_file.txt
reakwon 010-1234-1234   1981-01-01      M       100
nara    010-1010-2020   1993-12-12      M       20
kim     010-1234-4321   1977-07-17      M       69
nam     010-4321-7890   1996-06-20      M       75

 

4-3. 다중 조건을 활용하여 남자이면서 80점인 레코드 출력하기

여러 조건을 걸고 싶다면 어떻게 할까요? 다른 프로그래밍 언어와 유사하게 AND(&&) 조건과 OR(||) 조건을 활용할 수 있습니다. 여기서는 위의 두 조건을 && 묶어서 사용하면 됩니다. ||을 사용하면 OR조건으로 출력됩니다.

awk '{ if ( $4 == "M" && $5 >= 80) print ($0) }' ./awk_test_file.txt
reakwon 010-1234-1234   1981-01-01      M       100

 

5. 내장 함수

awk에는 여러가지 내장함수들이 존재합니다. 단어의 길이를 알아내려면 length함수, 단어의 부분단어를 추출하려면 substr함수를 사용할 수 있습니다. 아래의 awk 명령은 이름의 길이와 이름의 3글자까지 출력을 하는 명령입니다.

 awk '{ print ("name leng : " length($1), "substr(0,3) : " substr($1,0,3)) }' ./awk_test_file.txt

name leng : 4 substr(0,3) : nam
name leng : 7 substr(0,3) : rea
name leng : 3 substr(0,3) : sim
name leng : 4 substr(0,3) : nar
name leng : 3 substr(0,3) : yut
name leng : 3 substr(0,3) : kim
name leng : 3 substr(0,3) : nam

 

이밖에도 모두 소문자로 출력하는 tolower함수, 모두 대문자로 출력하는 toupper 도 있습니다. 문자열을 쪼개는 split함수도 존재합니다. 여러 내장함수들이 있으니 필요시에 따라 사용하면 됩니다. 무엇이 있는지 어떻게 보냐구요? man 1 awk를 보시면 됩니다.

6. 반복문

반복문도 사용할 수 있습니다. C언어를 배우시는 분에게는 너무 익숙한 문법으로 사용할 수 있습니다. 아래는 단지 별의미 없이 for문의 사용법을 보여드리려 print를 2번까지 진행한 것입니다. 

awk '{
for(i=0;i<2;i++)
 print( "for loop :" i "\t" $1, $2, $3)
}' ./awk_test_file.txt
for loop :0     name phone birth
for loop :1     name phone birth
for loop :0     reakwon 010-1234-1234 1981-01-01
for loop :1     reakwon 010-1234-1234 1981-01-01
for loop :0     sim 010-4321-4321 1999-09-09
for loop :1     sim 010-4321-4321 1999-09-09
for loop :0     nara 010-1010-2020 1993-12-12
for loop :1     nara 010-1010-2020 1993-12-12
for loop :0     yut 010-2323-2323 1988-10-10
for loop :1     yut 010-2323-2323 1988-10-10
for loop :0     kim 010-1234-4321 1977-07-17
for loop :1     kim 010-1234-4321 1977-07-17
for loop :0     nam 010-4321-7890 1996-06-20
for loop :1     nam 010-4321-7890 1996-06-20

 

7. BEGIN, END pattern

BEGIN은 awk가 모든 레코드를 돌기 전에 한번 action을 수행하고 END는 모든 레코드를 다 돈 후에 마지막으로 정의한 action이 실행됩니다. 아래의 예에서 어떻게 사용되는지 살펴봅니다.

8. 변수 사용

awk 역시 언어이기 때문에 변수를 사용할 수 있습니다. 만약 우리가 위의 파일에서 총점과 평균을 구하고 싶다면 어떻게 하면 좋을까요? 레코드를 돌면서 각각의 점수를 더하면서 총점을 구하고, 총점을 사람의 수로 나누면 되지요. 그래서 구한 결과는 레코드가 끝난 마지막에 출력하면 되니 아까 이야기했던 END 패턴을 앞에 명시해줍니다. 아래의 awk가 그 명령입니다. 여기서 cnt가 -1부터 시작인 이유는 실제 레코드가 아닌 헤더가 존재하기 때문입니다. 이건 인원으로 볼 수 없죠. (name phone  birth  sex  score)

 awk '
> BEGIN {
>  sum = 0
>  cnt = -1
> }
>
> {
>  sum += $5
>  cnt++
> }
>
> END {
>  avg = sum/cnt
>  print ("sum :" sum ", average :" avg)
> }' ./awk_test_file.txt

 

BEGIN에서는 초기화가 이루어지고, END에서는 결과를 출력해줍니다. 프로그래밍 언어를 배우셨다면 cnt++이나 sum +=$5의 구문이 무엇인지 아실겁니다. 모르시는 분은 구글 서치해보세요. 어렵지 않습니다.

 

이상으로 awk에 대한 사용법과 내용을 추출하는 여러가지 예제를 보았습니다. 깊이 알면 좋지만 저는 그렇게 깊이 알아서 사용할 일은 없어 여기까지만 설명하도록 하겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

링크에 대한 성질, 그리고 이에 대한 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

링크

파일은 서로 참조하고 참조 받을 수 있습니다. 그러니까 어떤 파일이 어떤 파일을 삿대질해서 가리킬 수도 있고(무척 평화적이죠? 현실세계에서는 참교육 시전받을 수 있습니다.) 같은 내용을 바라볼 수도 있다는 뜻입니다. 이러한 성질을 링크라고 합니다. 링크를 본격적으로 설명하기 전에 I-node라는 개념을 먼저 알아야 할 팔요가 있습니다.

1. Inode

Inode는 아래에 보는바와 같이 i-node라는 구조체를 가리키게 됩니다. 이 i-node에는 파일에 대한 거의 모든 정보(파일 모드, 링크개수, UID, GID, 파일 크기, 파일 관련 시간 등)가 담겨 있는데, 내용은 담겨있지 않습니다. 파일의 메타정보라고 생각하시면 됩니다. i-node는 파일의 내용을 가리키는 일종의 포인터를 갖고 있을 뿐이죠. Single indirect, Double indirect,  Triple indirect 등이 보이죠? 이게 다 파일에 대한 내용을 가리키는 블록들이며, 마치 C언어에서 포인터, 더블 포인터 등과 같이 내용을 가리키게 됩니다.

출처 : https://www.systranbox.com/exploring-the-essential-role-of-inodes-in-linux-and-unix-operating-systems/

inode가 갖고 있는 정보는 아래와 같으니 참고하시기 바래요.

inode가 가지고 있는 정보 설명
inode 번호 inode의 고유 식별 번호입니다.
파일 모드 16비트의 플래그로 파일의 실행 권한입니다. 소유자의 권한, 소유자 그룹의 권한, 기타 사용자의 권한, 파일 형식, 실행 플래그 등을 나타냅니다.
링크 수 이 아이노드에 대한 참조 수를 나타냅니다.
소유자 아이디 파일의 소유자 아이디를 나타냅니다.
그룹 아이디 파일 소유자의 그룹 아아디를 나타냅니다.
파일 크기 파일의 크기(bytes)를 나타냅니다.
파일 주소 실 데이터가 나오는 파일 주소를 나타냅니다.
마지막 접근 마지막으로 파일에 접근한 시간을 나타냅니다.
마지막 수정 마지막으로 파일을 수정한 시간을 나타냅니다.
아이노드 수정 마지막으로 아이노드를 수정한 시간을 의미합니다.

I-node는 각자 자신들의 고유한 번호를 가지고 있는데, 이 고유한 번호를 i-node 번호라고 합니다. ls 명령어에서 -i 옵션은 i-node번호를 같이 보여줍니다. 그래서 i-node번호와 같이 자세한 출력은 원하면 ls -li 을 입력하면 됩니다. 왼쪽에 나오는 숫자가 바로 i-node 번호입니다.

# ls -il /usr/lib
total 2952
6291630 drwxr-xr-x 105 root root   86016 Jun  8 06:17 aarch64-linux-gnu
6441489 drwxr-xr-x   2 root root    4096 Apr 18 00:48 apg
6291631 drwxr-xr-x   2 root root    4096 Feb 17 17:30 apparmor
6291632 drwxr-xr-x   5 root root    4096 Feb 17 17:30 apt
6442507 drwxr-xr-x   3 root root   12288 Apr 18 00:48 aspell

실제 inode의 내용을 확인해보고 싶다면 stat 명령을 사용해서 확인해보시기 바랍니다. stat명령도 내부적으로 inode에서 정보를 얻는 구현이 있기 때문입니다. 아래는 단순 빈 파일의 정보입니다.

# stat /etc/passwd
  File: /etc/passwd
  Size: 3212            Blocks: 8          IO Block: 4096   regular file
Device: fd00h/64768d    Inode: 5245840     Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2023-08-14 09:04:09.872000001 +0000
Modify: 2023-07-27 17:40:33.552013428 +0000
Change: 2023-07-27 17:40:33.560013428 +0000
 Birth: 2023-07-27 17:40:33.552013428 +0000

같은 I-node를 가리키느냐, 아니면 데이터 블록이 원본 파일을 가리키느냐에 따라서 하드링크(Hard link)와 심볼릭(Symbolic link)로 나뉘어집니다. 파일에 링크를 걸어주는 명령이 ln 명령어입니다. ln 명령어를 통해서 하드링크와 심볼릭 링크가 무엇인지 확인해봅시다.

       ln [OPTION]... [-T] TARGET LINK_NAME
       ln [OPTION]... TARGET
       ln [OPTION]... TARGET... DIRECTORY
       ln [OPTION]... -t DIRECTORY TARGET...

 

2. 하드링크

하드링크는 파일이 같은 i-node를 가리켜 파일의 내용을 공유하는 링크 방식을 의미합니다. 하드 링크는 같은 i-node를 가리키기 때문에 i-node 번호가 같습니다. ln 명령어로 하드링크를 걸어봅시다. 하드 링크를 거는 방법은 아래와 같은 명령어 형식을 사용하면 됩니다. 

ln 원본파일 링크파일

우선 아래와 같이 문자열을 담고 있는 origin이라는 파일을 하나 생성하고, i-node 번호(1열)를 확인하면 464235라는 것을 확인할 수 있네요. 파일을 최초로 만들었을 땐 링크수(3열)가 1이라는 점! 

# echo "hard link test" > origin
# ls -il
total 4
464235 -----w--w- 1 root ubuntu 15 Jun 14 01:35 origin

이후 ln 명령어를 통해서 하드링크를 걸어줍니다. I-node와 링크수를 확인해보니, i-node는 464235로 같고 링크수가 2로 하나 증가함을 알 수 있습니다. 그 외의 파일의 권한, 소유자, 시간 모두 정확히 같습니다. 그렇죠?

# ln origin  hardlink
# ls -il
total 8
464235 -----w--w- 2 root ubuntu 15 Jun 14 01:35 hardlink
464235 -----w--w- 2 root ubuntu 15 Jun 14 01:35 origin

그렇다면 우리는 이런 그림을 그려볼 수 있습니다. 링크수는 i-node를 가리키는 화살표의 갯수를 의미하며, 두 파일은 i-node를 같이 공유하고 있음을 알 수 있습니다.

2.1 하드링크의 원본 파일 삭제 시

그렇다면 만약 어느 하나의 파일을 삭제하게 된다면 어떻게 될까요? 여기서는 원본의 파일을 삭제해보도록 하겠습니다. 

# rm origin 
# ls -il
total 4
464235 -----w--w- 1 root ubuntu 15 Jun 14 01:35 hardlink
# cat hardlink 
hard link test

결과에서 알 수 있듯이 i-node의 번호는 변하지 않고, 링크의 갯수가 하나 줄었습니다. 내용을 보면 심지어 내용도 보존이 됩니다. 원본 파일을 삭제했는데도 말이죠. 이런 상황입니다. 

그래서 파일을 삭제한다는 뜻은 링크를 제거한다는 것과 유사합니다. 

 

3. 심볼릭 링크(Symbolic Link)

심볼릭 링크는 ln 명령어에 -s 옵션을 추가하여 링크를 걸 수 있습니다. 위와 같은 과정으로 테스트를 해보면 하드링크와 다른점을 확인할 수 있습니다.

# echo "symbolic link test" > origin
# ls -il
total 4
464235 -----w--w- 1 root ubuntu 19 Jun 14 01:54 origin
# ln -s origin symlink
# ls -il
total 4
464235 -----w--w- 1 root ubuntu 19 Jun 14 01:54 origin
464240 lrwxrwxrwx 1 root ubuntu  6 Jun 14 01:55 symlink -> origin

하드링크와의 차이점을 정리해보면 이렇습니다.

  • origin의 i-node 번호(1열)와 symlink 파일의 i-node번호가 다르다.
  • 링크의 갯수(3열)가 변하지 않는다.
  • 다른 기타 정보(권한, 크기, 시간 정보 등)이 다르다.
  • 파일의 유형을 보면 정규 파일('-')이 아니라 링크('l')이다.

이 상태에서 원본 파일과 심볼릭 링크 파일의 관계를 그리면요. 아래와 같습니다. 

이 그림이 성립이 된다는 것을 확인시켜드리고자 이 상태에서 원본 파일을 과감하게 지우겠습니다. 

# rm origin 
# ls -il
total 0
464240 lrwxrwxrwx 1 root ubuntu 6 Jun 14 01:55 symlink -> origin
# cat symlink 
cat: symlink: No such file or directory

원본 파일인 origin을 삭제하고 심볼릭 링크를 읽으려해보면 그러한 파일을 찾을 수 없다는 에러 메시지를 볼 수 있습니다.  만약 다시 origin을 생성해보면 어떨까요?

# echo "new origin" > origin
# ls -il
total 4
464235 -----w--w- 1 root ubuntu 11 Jun 14 06:59 origin
464240 lrwxrwxrwx 1 root ubuntu  6 Jun 14 01:55 symlink -> origin
# cat symlink 
new origin

이러한 경우에는 다시 symlink로 파일을 읽을 수 있습니다. 다만 그 내용은 원래 이전의 origin의 내용이 아니라 다시 생성한 origin 파일의 내용인 것을 알 수 있습니다. 결국은 심볼릭 링크의 경우에 파일을 가리킨다는 것을 확인할 수 있습니다. 

4. ln 명령어 구현 

간단한 ln 명령어를 구현해보도록 하겠습니다. 그전에 우리가 알아야할 관현 함수들은 symlink와 linkat 함수입니다. 

  • symlink
#include <unistd.h>
int symlink(const char *target, const char *linkpath);

심볼릭 링크를 거는 시스템 콜입니다. target은 현재 프로세스 위치의 파일 이름입니다. 상대 경로일 수 있습니다. linkpath는 링크 파일의 이름입니다. 이때 이 파일이 존재하면 에러 -1이 발생합니다. 

  • linkat
#include <unistd.h>
int linkat(int olddirfd, const char *oldpath,
			int newdirfd, const char *newpath, int flags);

하드 링크, 심볼릭 링크 둘 다 걸 수 있는 시스템 콜입니다. linkat 함수는 조금 까다로울 수 있는데, newpath가 oldpath를 가리키도록 링크를 걸게 됩니다.

olddirfd와 oldpath, newdirfd와 newpath가 한 쌍으로 파일을 나타냅니다. oldpath가 절대 경로일 경우 olddirfd는 무시가 되고, oldpath가 상대 경로일 경우 olddirfd 기준의 상대경로가 됩니다. newdirfd와 newpath와의 관계도 마찬가지입니다. 함수를 호출한 프로세스를 기준으로 상대위치로 만들고 싶다면 fd인자에 AT_FDCWD 상수를 사용해야합니다.

flag는 AT_SYMLINK_FOLLOW를 사용할 수 있습니다. 만약 이 flag를 쓰게 되면 원본 파일이 심볼릭 링크라면 링크를 따라가서 하드 링크를 겁니다. 원본 파일이라니까 헷갈리실거 같은데 ln 명령어에서 첫번째 인수를 말해요. 이 flag를 쓰지 않으면 원본 파일인 심볼릭 링크를 따라가 심볼릭 링크를 겁니다. 

ln 원본파일 링크파일

즉, 정리하면

  • AT_SYMLINK_FOLLOW 를 사용할 경우 : symlink를 계속 따라가서 harlink
  • AT_SYMLINK_FOLLOW를 사용하지 않은 경우 : symlink를 계속 따라가서 symlink

 

4.1 소스코드 

이제 간단히 구현한 myln.c 코드를 보겠습니다.

//myln.c

#include <unistd.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

// -s 옵션 
bool symbolic = false;
// -L 옵션
bool logical = false;

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

        int c;
        char **files_name;
        char *src, *dst;

        //옵션 파싱
        while((c = getopt(argc, argv, "sL")) != -1 ){
                switch(c){
                        case 's':
                                symbolic = true;
                                break;
                        case 'L':
                                logical = true;
                                break;
                }
        }

        files_name = argv + optind;

        //원본 파일
        src = files_name[0];
        //링크 파일
        dst = files_name[1];

        //심볼릭 링크시에 symlink 사용
        if(symbolic){
                return symlink(src, dst);
        }

        //심볼릭 링크가 아니면 -L 옵션 여부에 따라 AT_SYMLINK_FOLLOW 플래그 설정
        return linkat(AT_FDCWD, src, AT_FDCWD, dst, logical ? AT_SYMLINK_FOLLOW : 0);

}

linkat에 AT_FDCWD를 사용한 것을 보시기 바랍니다. AT_FDCWD는 현재 싱행되는 프로그램의 위치를 기준삼아 src를 찾고, dst링크 파일을 만듭니다. 위 코드는 실제 ln의 코드를 풀어 쓴 코드입니다. 아래는 실제 ln의 소스 코드 일부분입니다.

static bool
do_link (const char *source, const char *dest)
{
 ...
   ok = ((symbolic_link ? symlink (source, dest)
         : linkat (AT_FDCWD, source, AT_FDCWD, dest,
                   logical ? AT_SYMLINK_FOLLOW : 0))
        == 0);
 
 ...
 }

 

이제 코드 결과를 확인해봅시다. 

# gcc myln.c 
# echo "this is origin" > origin
# ./a.out -s origin symlink1
# ./a.out origin hardlink1
# ls -il
total 24
464557 -rwxr-xr-x 1 root root 9088 Aug 15 05:59 a.out
464558 -rw-r--r-- 2 root root   15 Aug 15 05:59 hardlink1
464648 -rw-r--r-- 1 root root 1121 Aug 15 05:52 myln.c
464558 -rw-r--r-- 2 root root   15 Aug 15 05:59 origin
464646 lrwxrwxrwx 1 root root    6 Aug 15 05:59 symlink1 -> origin

우선 원본 파일인 origin을 생성하고 심볼릭 링크와 하드링크를 하나씩 걸었습니다. symlink1은 심볼릭 링크로 잘 링크되어있고, hardlink1도 inode를 보니 origin과 같아서 하드링크가 잘 걸려있네요!

-L옵션도 확인해볼까요? 

# ./a.out -L symlink1 hardlink2
# ./a.out symlink1 symlink2
# ls -il
total 28
464557 -rwxr-xr-x 1 root root 9088 Aug 15 05:59 a.out
464558 -rw-r--r-- 3 root root   15 Aug 15 05:59 hardlink1
464558 -rw-r--r-- 3 root root   15 Aug 15 05:59 hardlink2
464648 -rw-r--r-- 1 root root 1121 Aug 15 05:52 myln.c
464558 -rw-r--r-- 3 root root   15 Aug 15 05:59 origin
464646 lrwxrwxrwx 2 root root    6 Aug 15 05:59 symlink1 -> origin
464646 lrwxrwxrwx 2 root root    6 Aug 15 05:59 symlink2 -> origin

-L옵션은 linkat 콜 인자 중 flag에 AT_SYMLINK_FOLLOW를 전달한 것과 같습니다. 그러니까 -L 옵션을 줄 경우 원본 파일은 symlink1을 따라가서 최종 원본 파일을 찾고 거기에 하드링크를 걸게 됩니다. -L 옵션을 뺀 경우, 그러니까 AT_SYMLINK_FOLLOW옵션을 주지 않았을 경우 링크를 계속 따라가서 최종 원본 파일에 심볼릭 링크를 걸게 되지요. 

여기까지 리눅스의 i-node가 대한 간단한 소개와 link의 방식인 hard link, symbolic link의 개념과 차이점을 확인해보았으며 직접 ln 명령어를 맛보기로 구현해보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

더 많은 정보를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

/etc/passwd(사용자 정보)

보안이나 리눅스를 배우신다면 /etc/passwd에 대한 이해는 필수입니다. 정보보안기사 시험에도 당골손님이기도 하죠.

/etc/passwd에는 시스템에 등록된 사용자의 정보들이 담겨있는 파일입니다. 이 파일을 이용해서 사용자의 계정과 인증을 관리하게 됩니다. 이 파일의 이름이 passwd인데, 여기에 패스워드 정보가 있는건 아니구요. 과거에 사용자의 패스워드를 이곳에 저장을 하였는데, /etc/shadow에 따로 암호화된 패스워드를 저장합니다. 과거에 패스워드를 저장했었던 것이 아직 파일 이름으로 남아 파일명이 passwd입니다.

/etc/passwd를 열어보면 아래와 같이 사용자의 정보들이 있습니다. 

/etc/passwd

 

각 필드들이 의미하는 바가 있는데, 필드 하나하나 무엇을 의미하는지 확인해봅시다. 각 필드는 콜론(:)으로 구분되어 집니다. 

     
        root:x:0:0:root:/root:/bin/bashroot:x:0:0:root:/root:/bin/bash
        

 

필드 설명
사용자 계정명 맨 앞에 필드는 사용자의 계정명을 나타냅니다
패스워드 그 다음의 필드는 패스워드 필드인데, x가 의미하는 바는 사용자의 패스워드가 /etc/shadow에 암호화되어 저장되어있다는 뜻입니다.
UID 사용자의 user id를 나타냅니다. 관리자 계정(Root)은 UID가 0입니다.
GID 사용자의 그룹 ID를 나타냅니다. 관리자 그룹(Root)의 GID는 0입니다.
comment 사용자와 관련한 기타 정보로 일반적으로 사용자의 이름을 나타냅니다.
홈 디렉토리 사용자의 홈디렉토리를 의미합니다. 관리자 계정의 홈 디렉토리는 /root이며, 다른 사용자의 홈 디렉토리는 기본으로 /home/ 하위에 계정명으로 위치합니다.
로그인 쉘 사용자가 로그인시에 사용할 쉘을 의미합니다. 보통 사용자의 쉘은 성능이 우수한 bash쉘을 사용합니다. 로그인이 불필요한 계정도 있는데요. 이때는 이 필드가 /usr/sbin/nologin, /bin/false, /sbin/nologin 등으로 표기됩니다. 이것은 사용자가 아니라 어플리케이션이기 때문이죠. 아래처럼 말이죠.

sshd:x:126:65534::/run/sshd:/usr/sbin/nologin
gnome-initial-setup:x:124:65534::/run/gnome-initial-setup/:/bin/false
gdm:x:125:130:Gnome Display Manager:/var/lib/gdm3:/bin/false


 

 

 

/etc/shadow

/etc/shadow에는 암호화된 패스워드와 패스워드 설정 정책이 기재되어 있습니다. 여기서 관리자 계정과 관리자 그룹만이 이 파일을 읽을 수 있습니다. 이 파일이 만약 평문의 비밀번호의 정보를 가지고 있다면 모든 사용자의 비밀번호가 유출될 수 있습니다.

/etc/shadow

 

/etc/shadow 파일의 root의 정보를 보게되면 아래와 같이 나오게 되는데요. 각 필드에 대해 설명해보도록 하겠습니다.

/etc/shadow field

필드 설명
사용자 계정명 맨 첫 필드는 사용자의 계정명을 뜻합니다.
암호화된 패스워드 두번째 필드는 암호화된 패스워드를 뜻합니다. 이 패스워드는 또 $로 필드가 구분되어 있는데요. 아래처럼 구분이 됩니다.

$algorithm_id$salt$encrypted_password

algorithm_id : 암호학적 해시의 id를 의미하는데요. id는 아래와 같습니다.
1 : MD5(가장 취약한 일방향 해쉬로 요즘에는 쓰지 않습니다.
2 : BlowFish
5 : SHA-256
6 : SHA-512
제 시스템은 SHA-512를 사용하는 것을 알 수 있습니다.

salt : 각 해쉬에 첨가할 랜덤값입니다. 이 랜덤값에 따라서 해시의 값이 바뀌게 됩니다.
encrypted_password : 마지막은 알고리즘과 salt로 패스워드를 암호화한 값입니다.

이 뿐만 아니라 패스워드 필드에 *, !!, 또는 빈값이 설정될 수 있습니다. 이것이 의미하는 바를 알아보겠습니다.
* : 패스워드가 잠긴 상태입니다. 로그인은 불가합니다. 별도의 인증방식을 사용하여 로그인을 할 수는 있습니다.
! : 패스워드가 잠긴 상태이고 로그인을 할 수 없습니다. 또는 사용자를 생성하고 패스워드를 설정하지 않은 상태이기도 합니다.
empty : 패스워드가 설정되지 않았지만 로그인이 가능합니다. 
마지막 변경 마지막으로 패스워드를 변경한 날을 1970년 1월 1일 기준으로 일수도 표시합니다. 여기서 마지막으로 패스워드를 변경한 날은 1970년 1월 1일 이후 18693일이 지났음을 알 수 있습니다.
패스워드 최소 사용기간 패스워드의 최소 사용기간 설정으로 패스워드를 변경한 이후 최소 이 정도의 기간은 써야한다는 것을 의미합니다. 그래서 마지막 변경이 일어난 후 이 일수가 지나기 전에 암호를 변경할 수 없습니다.
패스워드 최대 사용기간 패스워드의 최대 사용기간 설정으로 마지막 패스워드 변경 이후 만료일수를 의미합니다. 패스워드를 변경하지 않으면 공격자가 패스워드를 깰 수도 있으므로 90일을 권장합니다.
경고 패스워드 만료 이전에 경고할 경고 일수를 의미합니다.
비활성화 패스워드가 만료된 이후에 계정이 잠기기 전까지 비활성 일수(date)입니다. 해당 비활성 기간동안에 패스워드를 변경해야 계정이 잠기지 않습니다. 
만료일 계정 만료일 필드입니다. 1970년 1월 1일 기준으로 일수로 표시합니다.

 

이상으로 /etc/shadow와 /etc/passwd에 대해서 알아보았는데, 이 파일은 매우 중요하므로 보안을 배우는 사람은 잘 알아두어야합니다. 

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

더 많은 정보를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

쉘스크립트 기본

쉘 스크립트는 쉘에게 무슨 명령들을 실행할지 알려주는 스크립트 파일입니다. 여기서는 가장 널리쓰이는 bash 쉘을 사용하는 스크립트를 설명하도록 하겠습니다.

#!/bin/bash

스크립트 최상단에는 항상 이 구문이 적혀있어야합니다. 간단하게 hello,world라는 문자열을 출력하는 스크립트를 만들어봅시다. 파일명은 exam.sh로 정해놓고요.

#!/bin/bash

echo "hello, world"
printf "hello, world"

 

./exam.sh로 스크립트를 실행하면 되는데 혹시 실행이 되지 않는다면 실행 권한이 없어서 그런것이니까 chmod 777 exam.sh로 권한을 바꾸어주면 됩니다. 그 후 결과를 보면 echo는 개행이 되지만 printf는 개행이 되지 않는 다는 것을 알 수 있습니다. 또 C언어 처럼 문자열을 전달할 수도 있는데 포맷 문자 %s를 이용하면 됩니다.

printf "%s, %s" hello, world

 

1) 변수(Variable)

쉘스크립트에서도 변수를 사용할 수 있는데요. 변수명=값 으로 변수를 선언하고, 사용할때는 앞에 $를 붙여줍니다. 중괄호를 이용하는 방법도 있습니다. ${변수}식으로 중괄호로 묶어서 사용합니다.

#!/bin/bash

h="hello"
w="world"
echo "${h}, ${w}"

 

여기서 export를 사용하여 다른 쉘 스크립트에서 사용할 수 있게 합니다. exam.sh는 변수 선언하고 export를, exam2.sh는 그 변수를 출력하는 쉘 스크립트입니다.

#!/bin/bash
#exam.sh
export MY_VAR="reakwon"

./exam2.sh

 

#!/bin/bash
#exam2.sh
echo ${MY_VAR}

 

매개변수

프로그램에서도 실행할때 인자를 주듯 쉘 스크립트도 역시 그렇게 할 수 있습니다. 실행한 스크립트 이름은 ${0}, 그 이후는 전달받은 인자 값들입니다(${1}, ${2}, ...). 아래의 예제코드를 보면서 이해하실 수 있습니다.

#!/bin/bash


echo "script name:${0}"
echo "매개변수 갯수 :${#}"
echo "전체 매개변수  값 : ${*}"
echo "전체 매개변수 값2 : ${@}"
echo "매개변수 1 : ${1}"
echo "매개변수 2 : ${2}"

 

 

 

 

 

 

 

 

예약변수

쉘 스크립트에서 사용자가 정해서 만들 수 없는 이미 정의된 변수가 존재합니다. 그것을 예약 변수라고 하는데요. 몇가지 예약 변수를 보도록 합시다.

변수 설명
HOME 사용자 홈 디렉토리를 의미합니다.
PATH 실행 파일의 경로입니다. 여러분이 chmod, mkdir 등의 명령어들은 /bin이나 /usr/bin, /sbin에 위치하는데, 이 경로들을 PATH 지정하면 여러분들은 굳이 /bin/chmod를 입력하지 않고, chmod 입력만 해주면 됩니다.
LANG 프로그램 실행 시 지원되는 언어를 말합니다.
UID 사용자의 UID입니다.
SHELL 사용자가 로그인시 실행되는 쉘을 말합니다.
USER 사용자의 계정 이름을 말합니다.
FUNCNAME 현재 실행되고 있는 함수 이름을 말합니다.
TERM 로그인 터미널을 말합니다.

 

2) 배열(Array)

쉘 스크립트에서 배열은 1차원 배열만 지원하며 중괄호를 사용해야합니다. 배열 원소는 소괄호안에 공백으로 구분하여 줍니다. 배열 안 원소는 문자열이든 정수든 상관 없이 쓸 수 있는 특징이 있습니다.

#!/bin/bash

arr=("hello" "world" 1 2 3 4 5)

echo "배열 전체 : ${arr[@]}"
echo "배열 원소의 갯수 : ${#arr[@]}"
echo "배열 첫번째 : ${arr}, 혹은 ${arr[0]}"
echo "5번 index를 갖는 배열의 원소 : ${arr[5]}"

arr[5]="five"

echo "5번 index를 갖는 배열의 원소 : ${arr[5]}"

# 5번 원소 해제
unset arr[5]
echo "5번 원소 삭제 후"
echo "5번 index를 갖는 배열의 원소 : ${arr[5]}"
echo "6번 index를 갖는 배열의 원소 : ${arr[6]}"

 

3) 함수(Function)

다른 프로그래밍 언어와 같이 쉘 스크립트에서도 함수를 사용할 수 있습니다. 함수의 정의 형식은 아래와 같습니다. 함수명 앞 function은 써주지 않아도 무관합니다. 주의 할 점은 함수가 호출되기 전에 함수가 정의되어 있어야하며 호출할때는 괄호를 써주지 않고 호출해야한다는 점입니다.

function 함수명(){
 ... 내용 ...
}
#!/bin/bash

function func(){
        echo "func()"
}

#함수 호출
func

 

4) 비교문

쉘스크립트에서 비교문은 약간 특이합니다. 아래와 같은 형식으로 비교문을 사용할 수 있습니다. 

if [ 값1 조건 값2 ]; then
	... 작업 내용 ...
fi

조건은 -eq, -gt, -lt 등이 있는데, 자세한 것은 아래의 코드에서 보도록 합시다.

#!/bin/bash

function func(){
        a=10
        b=5

        if [ ${a} -eq ${b} ]; then
                echo "a와 b는 같다."
        fi

        if [ ${a} -ne ${b} ]; then
                echo "a와 b는 같지 않다."
        fi

        if [ ${a} -gt ${b} ]; then
                echo "a가 b보다 크다."
        fi

        if [ ${a} -ge ${b} ]; then
                echo "a가 b보다 크거나 같다."
        fi

        if [ ${a} -lt ${b} ]; then
                echo "a가 b보다 작다."
        fi

        if [ ${a} -le ${b} ]; then
                echo "a가 b보다 작거나 같다."
        fi

}

#함수 호출
func

 

 

 

 

 

 

쉘스크립트는 명령어를 다루는 스크립트이기 때문에 파일이 존재하는지, 디렉토리가 존재하는지 등 파일과 관련된 조건 여부도 조건문을 통해 알 수 있습니다. 아래는 그것을 정리한 표입니다. 그리고 앞에 느낌표(!)를 써주고 조건을 기재하면 그 조건이 false일때 참이 성립합니다.

파일 관련 조건

조건 설명
if [ -d ${변수} ]; then 
if [ ! -d ${변수} ]; then
${변수}의 디렉토리가 존재하면 참이 성립합니다. 
${변수}의 디렉토리가 존재하지 않으면 참이 성립합니다. 
if [ -e ${변수} ]; then 
if [ ! -e ${변수} ]; then 
${변수}라는 파일이 존재하면 참입니다.
${변수}라는 파일이 존재하지 않으면 참입니다.
if [ -L ${변수} ]; then 파일이 symbolic link이면 참입니다.
if [ -s ${변수} ]; then 파일의 크기가 0보다 크면 참입니다.
if [ -S ${변수} ]; then 파일 타입이 소켓이면 참입니다.
if [ -r ${변수} ]; then 파일을 읽을 수 있으면 참입니다.
if [ -w ${변수} ]; then 파일을 쓸 수 있으면 참입니다.
if [ -x ${변수} ]; then 파일을 실행할 수 있으면 참입니다.
if [ -f ${변수} ]; then 파일이 정규 파일이면 참입니다.
if [ -c ${변수} ]; then 파일이 문자 장치이면 참입니다.
if [ ${변수1} -nt ${변수2}]; then 변수1의 파일이 변수2의 파일보다 최신 파일이면 참입니다.
if [ ${변수1} -ot ${변수2}]; then 변수1의 파일이 변수2의 파일보다 최신이 아니면 참입니다.
if [ ${변수1} -ef ${변수2}]; then 변수1의 파일과 변수2의 파일이 동일하면 참입니다.

 

만약 AND조건과 OR조건을 섞어서 쓴다면 어떻게 할까요? AND는 -a, OR은 -o를 이용해서 조건을 추가할 수 있습니다.

function func(){
        a=aa
        b=bb
        if [ -f ${a} -a -d ${b} ]; then
                echo "a는 파일이고 b는 디렉토리 "
        fi
}

#함수 호출
func

 

또한 여러 조건을 걸려면, 즉, C언어에서 else if역할을 하는 것은 elif라는 것을 사용하여 else if와 똑같은 역할을 할 수 있게 해줍니다. elif[ 조건 ]; then 이런식으로 쓰시면 됩니다.

만약 반복문에서 사용하여 조건이 참일때 반복문을 멈추고 싶을때 break라는 키워드를 사용하여 반복문을 멈출 수 있습니다.

C에서 switch case와 동일한 구문을 하는 문법도 존재합니다. 아래와 같이 case문을 사용하면 됩니다. 각 case의 끝을 보면 세미콜론 2개로 종료하는 것을 볼 수 있습니다.

#!/bin/bash

case ${1} in
        "linux") echo "리눅스" ;;
        "unix") echo "유닉스" ;;
        "windows") echo "윈도우즈" ;;
        "MacOS") echo "맥OS" ;;
        *) echo "머야" ;;
esac

5) 반복문

반복문 중 for문에 대한 설명은 예제와 같이 보는 것으로 하겠습니다. 

#!/bin/bash

function func(){
        echo "사용예1"
        for i in 1 2 3 4 5
        do
                echo "${i}"
        done

        echo "사용예2"
        list="1 2 3 4 5"
        for i in ${list}
        do
                echo "${i}"
        done

        echo "사용예3"
        for i in {1..5}
        do
                echo "${i}"
        done

        echo "사용예4: 크기를 2만큼 증가시키면서 출력"
        for i in {1..5..2}
        do
                echo "${i}"
        done

        echo "사용예5: 배열을 이용"
        arr=(1 2 3 4 5)
        for i in "${arr[@]}"
        do
                echo "${i}"
        done


        echo "사용예6: C와 유사한 형식의 for문"
        for ((i=0; i<5; i++)); do
                echo "${i}"
        done
}

#함수 호출
func

 

 

 

 

 

6) 명령어의 종료 코드(exit)

만약 명령어가 실패할 경우 동작을 다르게 할것이라면 어떻게 할까요? 명령어의 종료 코드를 얻어오면 됩니다. 명령어의 종료 코드는 $?에 가장 최근 실행한 명령어의 종료 코드가 있습니다. 정상적인 종료는 0이 반환되는데, 만약 0이 아닌 값으로 반환되면 오류라고 판단하시면 됩니다. 여기에 간단한 오류를 내는 c파일(main.c)을 작성하고 gcc로 컴파일(gcc main.c)합니다. 이 c파일은 실행시 전달인자가 2개 보다 작으면 exit으로 코드 16을 반환합니다.

#include <stdlib.h>
#include <stdio.h>
int main(int argc, char* argv[]){
        if(argc<2){
                fprintf(stderr,"인자는 2개 이상이어야 합니다.\n");
                exit(16);
        }
}

 

그리고 쉘스크립트를 아래와 같이 수정해서 오류의 코드가 얼마인지 확인해보면 우리가 exit에 넘겨준 16이 찍히는 것을 확인할 수 있습니다.

#!/bin/bash

function func(){
        ./a.out
        echo "오류 코드 ($?)"
}

#함수 호출
func

 

./a.out에 인자를 하나 넣어서 다시 실행시켜보세요. 0이 종료코드로 반환 될 것입니다. 이렇게 이전에 명령어의 결과를 봐야하는 경우 $?으로 판단하여 분기시킬 수 있습니다.

이렇게 종료 반환 코드는 명령어 뿐만 아니라 쉘 스크립트 자체도 반환할 수 있습니다. 즉, 쉘 스크립트의 수행이 실패했는지 알려주기 위해서는 exit 명령어를 사용하면 됩니다. 아래의 예가 그 사용법을 보여줍니다. 우선 exam2.sh라는 파일을 만들어봅시다. 이 쉘 스크립트 파일은 exam.sh에서 실행할 예정입니다. 별거없이 16의 종료코드를 반환하며 끝이 납니다. 

#!/bin/bash

exit 16

 

이 쉘 파일을 exam.sh에서 실행시켜볼까요?

#!/bin/bash

function func(){
        ./exam2.sh
        echo "오류 코드 ($?)"
}

#함수 호출
func

 

확인해보면 우리가 기재했던 16이 출력되는 것을 확인할 수 있습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

리눅스에 대한 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

리눅스 커널(Kernel)

리눅스 커널은 하드웨어와 가장 가까이 있는 일종의 프로그램입니다. 가장 핵심적인 프로그램으로 사용자는 커널을 통해서 하드웨어를 제어합니다. 함부로 하드웨어를 직접 만졌다가는 돌아올 수 없는 강을 건너게 될 수도 있습니다. H/W에는 CPU, 메모리(RAM), 하드디스크(HDD), 기타 입출력 장치 등 많은 것들이 있는데 실제 직접 조작할 수도 없고 해서도 안됩니다. 그래서 커널이 존재하게 되는것이지요. 이 처럼 커널은 시스템의 자원을 관리하는 운영체제 그 자체로 보시면 됩니다. 

윈도우즈도 역시 커널을 가지고 있는데, 사용자가 커널의 코드를 직접 들여다 볼 수 없고 거기에 맞는 모듈(Module)을 만들어거 끼워넣을 수 없죠. 윈도우즈는 커널 내부를 공개하고 있지 않기 때문인데요. 그래서 윈도우즈 커널을 수정하려면 오직 MS사 밖에 할 수 없습니다. 리눅스는 윈도우즈와는 다르게 커널 내부 소스를 전부 공개합니다. 그래서 커널을 잘 아는 사람은 거기에 맞게 모듈이라는 커널의 부분을 끼워넣어서 변경할 수 있습니다. 

이전에 리눅스는 커널에 조금이라도 변경이 생기면 수정 후 전체 커널 컴파일을 해야했는데, 이 모듈이라는 개념이 도입된 후 필요한 커널의 부분만 수정하고 다시 끼울 수 있으면서 커널 컴파일하는 모든 과정을 수행하지 않아 시간이 획기적으로 줄어 들 수 있습니다. 현재 적재된 커널 모듈을 보려면 lsmod를 이용해서 볼 수 있고 모듈을 설치할때는 insmod를 이용하면 되는데, 물론 아무나는 안됩니다. 커널 프로그래밍을 이용하기 때문에 아무나 건드리면 시스템이 멈출 수 있습니다.

쉘(shell)

쉘은 사용자의 응용프로그램과 커널 사이에 위치해있으며 응용프로그램의 명령어와 커널이 대화를 하도록 만들어줍니다. 그래서 명령어 해석기라는 부릅니다.

자, 우리가 명령어를 입력하게 되면 컴퓨터에서는 쉘(shell)이 이 명령어를 받아 해석하여 커널(kernel)에게 보내면 커널은 우리가 내려주었던 동작을 하게 됩니다. 그래서 그에 대한 결과를 사용자에게 전달하려고 다시 쉘에 응답을 보내고 쉘을 거쳐 사용자에게 도달합니다. 여기서 쉘도 프로그램입니다.

쉘 종류는 여러가지고 존재합니다. 

Bourne Shell (/bin/sh)

본쉘이라고 합니다. 유닉스 최초의 쉘이기도 하지요. 최조의 쉘이라서 미흡한 기능이 많은데요. 여기서 일반 유저의 쉘 프롬프트는 $, root사용자의 프롬프트는 #으로 나타냅니다.

Bash, Bourne Again Shell (/bin/bash)

현재 리눅스에서 표준으로 채택된 쉘입니다. 여러분에게 가장 친숙한 쉘이라고 생각하시면 됩니다. 여러분이 명령을 칠때 명령어 다 쓰기 싫어서 tab쓰시죠? 그게 원래는 리눅스 자체의 기능이 아니고 bash의 기능입니다. 그 밖에도 아래와 같은 특징이 있습니다. Mac 운영체제에서도 bash가 쓰입니다.


 - History(방향키 ↑↓)
 - Alias
 - 자동 완성 (tab)
 - 연산
 - Job Control
 - 그 외

 

C shell (/bin/csh)

C언어를 기반으로 만들어진 쉘입니다. 쉘 스크립트가 C언어로 작성하는 것처럼 작성할 수 있습니다.

 

 

쉘을 이해해보자

이 외에도 쉘의 종류는 여러가지가 있는데요. 실제 시스템에서 지원하는 쉘을 보기 위해서 /etc/shells를 열어보시면 됩니다. 아래는 저의 시스템에 있는 shell들입니다.


/home/ubuntu# cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/bin/bash
/usr/bin/bash
/bin/rbash
/usr/bin/rbash
/bin/dash
/usr/bin/dash

그리고 사용자가 어떤 쉘을 쓰고 있는지 보고 싶으시다면 SHELL 환경변수를 확인하면 됩니다. echo $SHELL 으로 확인하세요. 저는 bash쉘을 사용하고 있군요.


/home/ubuntu# echo $SHELL
/bin/bash

 

여기서 bash쉘에서 지원하는 기능이 sh(본쉘)에서도 먹힐까요? 사용하는 bash쉘을 본쉘로 바꿔보도록 하겠습니다. 단순히 sh명령으로 쉘을 실행해봅시다. 그리고 명령어를 아무거나 처보세요. 그리고 우리가 이전의 명령어를 치려고 위 화살표(↑)를 누르면 이상한 문자(^[[A)가 쓰여지게 되어 이전의 명령어를 수행할 수는 없습니다. 결국 sh는 히스토리 기능을 지원하지 못하며, 정확히 얘기해서 저의 명령을 해석하지 못한다는 것입니다. bash에서는 ~/.bash_history에 그 동안의 명령어들 내역을 보관하고 있는데, 본쉘은 ~/.bash_history가 무엇인지도 모르죠.


/home/ubuntu# sh
# ls
공개  다운로드  문서  바탕화면  비디오  사진  음악  템플릿
# pwd
/home/ubuntu
# ^[[A

 

보통 bash에서 ll은 alias로 ls -alF의 명령어 별명으로 지정합니다. 지금 bash에서 ll을 치면 파일들의 목록이 자세히 나오게 되는데요. ll을 sh상에서 실행하면 실행이 되지 않습니다. bash에서 ll이 수행되지 않는다면 alias지정이 되지 않은 것인데요. vi로 ~/.bashrc를 열고 하단에 alias ll='ls -alF'를 써주시고 source ~/.bashrc를 수행하면 이제 ll이 수행될 것입니다. bashrc는 bash쉘의 설정파일입니다. 

 
     80 # some more ls aliases
     81 alias ll='ls -alF'
     82 alias la='ls -A'
     83 alias l='ls -CF'
     84

그렇다면 사용자가 접속할때 bash말고 다른 쉘을 사용하고 싶다면 어떻게 할까요? 시스템에서 root로 접속시 기본적으로 사용할 쉘이 어디에 기록되어있냐면 /etc/passwd에 기록되어있습니다. cat /etc/passwd | grep root의 결과가 아래와 같습니다. 리눅스를 조금 공부하셨다면 /etc/passwd에서 각 필드가 무엇을 뜻하는지는 아실겁니다. 

root:x:0:0:root:/root:/bin/bash

맨 오른쪽에 보이는 것이 접속 시 적용할 쉘인데요. 바꾸고 싶다면 chsh -s [shell path]를 입력하면 됩니다. 예를 들면 chsh -s /bin/sh 이렇게 쓰고 재접속을 하면 적용된 쉘로 바뀝니다. 

쉘에서 쓰는 명령어들을 하나의 대본으로 만들어 놓을 수 있는데요. 예를 들어 어느 경로에 들어가서 디렉토리를 만들고 거기에 파일을 만들고 파일의 내용을 어떤 내용으로 기록하고, 그런 작업들 말이에요. 그러려면 사용자가 수동으로 cd를 입력하고 mkdir하고 touch해서 파일을 만들고 그 내용을 echo '블라블라' > file 해서 입력하고 해야겠지요. 하지만 이런 대본(script)를 만들어서 쉘에게 알려주면 일련의 명령을 쉘이 알아서 수행해줍니다. 이것을 우리는 쉘 스크립트라고 하며 확장자가 .sh로 끝나는 파일입니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

스케줄링의 개념

단일 처리 시스템에서는 실행 중인 프로세스(A)가 존재하는데 다른 프로세스(B)가 입출력을 요청하면 그 프로세스(B)는 이전의 프로세스(A)의 자원을 놓을때까지 대기하고 있어야합니다. 하지만 다중 프로그래밍에서는 여러 프로세스들이 동시에 돌아갈 수 있으며, 프로세스가 자원(프로세서 등)을 요청하면 운영체제는 그 자원을 적절히 분배하여 프로세스에게 할당합니다. 그래서 다음과 같은 장점을 얻을 수 있습니다.

1. CPU의 활용 극대화

2.프로세스 처리율(시간 당 작업량)을 늘릴 수 있습니다.

프로세스는 필요한 자원을 할당받기 위해 큐에 대기합니다. 그래서 그 큐에 있는 프로세스를 어떻게 스케쥴링하는지가 프로세스 스케줄링 알고리즘이라고 합니다. 그레서 어떤 스케줄링 알고리즘이 있는지 봅시다. 

잠깐 프로세스 스케줄링에 관한 용어 몇가지만 알아보고 가도록 합시다. 대기 시간, 실행 시간, 반환 시간입니다. 

용어 설명
대기 시간 자원의 할당을 대기하는 시간을 의미합니다.
실행 시간 실제로 프로세스가 자원을 할당받은 다음 작업을 수행하는 시간을 의미합니다.
반환 시간 작업을 완료하는데 소요되는 전체 시간으로 대기 시간과 실행 시간을 모두 포함합니다. 

 

1 선입선처리 스케줄링(First Come First Served, First In First Out)

선입선처리(FCFS, FIFO) 알고리즘은 말 그대로 먼저 요청한 프로세스가 먼저 자원을 제공받으며 이미 사용중이라면 사용이 끝날때까지 기다려야하는 스케줄링 방식입니다.

프로세스 5개가 있고, 각가 P1의 실행시간 10, P2의 실행시간 5, P3의 실행시간 6, P4의 실행시간 9, P5의 실행시간 10이라고 할때 반환 시간을 알아보도록 하죠.

프로세스 도착 시간 실행 시간
P1 0 10
P2 1 28
P3 2 6
P4 3 4
P5 4 14

 

먼저 요청한 프로세스가 먼저 제공받으니 P1부터 차례대로 자원을 할당받습니다. 그래서 프로세스의 반환시간과 대기시간을 구하면 아래와 같습니다.

로세스 반환시간 대기시간
P1 10 0
P2 37 9
P3 42 36
P4 45 41
P5 58 44
평균 평균 반환 시간
(10+37+42+45+58)/5 = 38.4
평균 대기 시간
(0+9+36+41+44)/5 = 
26
장점 1) 스케줄링이 단순합니다.
2) 모든 프로세스가 실행될 수 있습니다.
3) 프로세서가 지속적으로 유용한 프로세스를 수행하여 처리율이 높습니다.
단점 1) 비선점방식의 스케줄링이므로 대화형 작업에는 부적합합니다.
2) 만약 어떤 프로세스의 수행시간이 길면 대기시간이 늘어납니다. 그래서 짧고 간단한 작업은 계속 기다려야합니다.

2. 최소 작업 우선 스케줄링(Shortest Job First)

프로세스의 실행 시간을 이용하여 가장 짧은 시간을 갖는 프로세스가 먼저 자원을 할당받는 방식입니다. 이 방식은 선점할 수도 있는 스케줄링 방식입니다. 이전에 FIFO방식은 중간에 다른 프로세스가 들어오면 그 프로세스는 대기해야했죠? 이 스케줄링 방식은 선점 또는 비선점이 가능합니다. 위와 같은 프로세스라고 할때 비선점형 SJF 스케줄링의 결과는 아래와 같습니다.

로세스 반환시간 대기시간
P1 10 0
P2 61 33
P3 18 12
P4 11 7
P5 30 16
평균 평균 반환 시간
(10+61+18+11+30)/5 = 26
평균 대기 시간
(0+33+12+7+16)/5 = 13.6

 

자 P1이 먼저 요청해서 자원을 할당받아 놓은 상태인데, P2가 도착합니다. 비선점형이기 때문에 P2는 우선 기다리고 있는데 P1의 작업이 끝나기 전에 P3, P4 ,P5의 요청이 도착하네요. 그러면 실행 시간이 가장 작은 P4가 자원을 할당받아 쓰고 그 다음인 P3이 자원을 할당받아 사용합니다. 이렇게 되면 위와 같은 결과가 도출되지요.

그렇다면 비선점형 SJF는 어떨까요? 아래의 표가 선점형 SJF의 결과입니다.

로세스 반환시간 대기시간
P1 20 10
P2 61 33
P3 10 5
P4 4 0
P5 30 16
평균 평균 반환 시간
(20+61+10+4+30)/5 = 25
평균 대기 시간
(10+33+4+0+16)/5 = 12.6

이해를 돕기 위해서 아래의 간트 차트를 보세요.  P1이 먼저 도착해서 수행하고 있는 와중에 P2가 도착하는데요. P2는 수행되어야할 시간이 P1보다 크니 P1은 계속 작업을 수행합니다. 그러다가 P3가 도착하는데 P1이 수행해야할 시간보다 P3의 수행시간이 더 짧으니 P3가 작업을 수행합니다. 그런데 P4가 다음에 도착하네요. P4가 더 적은 시간을 갖으니 P4를 수행합니다. 이때 P4가 끝나면 남아있는 프로세스 중에서 가장 적은 수행시간을 갖는 P3의 작업을 이어서 하게 됩니다.

장점 1) 항상 짧은 작업을 먼저 처리하게 되니까 평균 대기시간이 가장 짧습니다.
단점 1) 수행시간이 긴 작업은 짧은 작업에 밀려 기아가 발생합니다.
2) 실행 시간을 예측할 수 없어 실용적이지 못합니다.
3) 짧은 작업이 먼저 실행되므로 공정하지 못한 정책입니다.

 

 

 

3. 우선순위 스케줄링

우선순위 스케줄링은 프로세스마다 우선순위라는 속성이 붙게 됩니다. 우선순위 스케줄링도 역시 선점, 비선점형으로 스케줄링이 가능합니다. 숫자가 높을 수록 우선순위가 높고 만약 우선순위가 같다면 FIFO방식으로 동작합니다.

프로세스 도착 시간 실행 시간 우선순위
P1 0 10 3
P2 1 28 2
P3 2 6 4
P4 3 4 1
P5 4 14 2

 

그래서 아래는 선점형에서의 결과를 나타낸 표입니다. 약간 헷갈릴 수 있을텐데요. P1 먼저 수행하다가 P3가 오면 우선 순위가 P3가 더 높으니까 P1은 대기하고 P3가 작업을 수행합니다.

로세스 반환시간 대기시간
P1 16 6
P2 43 15
P3 6 0
P4 59 55
P5 54 40
평균 평균 반환 시간
(16+43+6+59+54)/5 = 35.6
평균 대기 시간
(6+15+0+55+40)/5 = 23.2

다음은 비선점형일때의 결과 표입니다.

로세스 반환시간 대기시간
P1 10 0
P2 43 15
P3 14 8
P4 59 55
P5 54 40
평균 평균 반환 시간
(10+43+14+59+54)/5 = 36
평균 대기 시간
(0+15+8+55+40)/5 = 23.6

 

만약 우선순위가 낮은 프로세스가 높은 프로세스에 의해 실행이 되고 있지 않은 상황이라면 어떻게 될까요? 이때는 그 프로세스의 우선순위를 점차 높여 처리받게끔 하는 에이징이라는 기법을 사용합니다.

장점 1) 각 프로세스의 중요성에 따라서 작업을 수행하기 때문에 합리적입니다.
2) 실시간 시스템에서 사용가능합니다.
단점 1) 높은 우선순위를 갖는 프로세스가 계속적으로 스케줄링이 되면 우선순위가 낮은 프로세스는 자원을 할당받지 못하기 때문에 기아가 발생할 수 있습니다.

4. 라운드 로빈 스케줄링(Round-Robin)

라운드 로빈 스케줄링은 시분할 시스템을 위해 설계된 스케줄링 기법입니다. 이 스케줄링은 작은 단위의 시간인 시간 할당량(Time-Slice)을 정의하여 그 시간 만큼 자원을 할당하는 방식입니다. 그래서 그 시간안에 작업을 끝내지 못하면 다음 프로세스가 다시 그 시간만큼 자원을 할당받아씁니다. 여기서 시간 할당량을 5로 정하고 간트 차트와 결과를 보면 아래와 같습니다. 

각 프로세스들은 공정하게 5만큼의 시간 동안 작업을 진행하는 것을 볼 수 있네요. 

로세스 반환시간 대기시간
P1 29 19
P2 61 33
P3 33 27
P4 16 7
P5 45 31
평균 평균 반환 시간
(29+61+33+16+45)/5 = 36.8
평균 대기 시간
(19+33+27+7+31)/5 = 23.4
장점 1) 모든 프로세스가 공정하게 스케줄링을 받을 수 있습니다.
2) 실행 큐에 프로세스의 수를 알고 있을때 유용합니다.
3) 프로세스의 짧은 응답시간을 갖고 최악의 응답시간을 알 수 있습니다.
4) 평균 대기시간이 FIFO와 SJF보다 적습니다.
단점 1) 성능은 규정 시간량의 길이에 따라 달라지므로 작업이 비슷한 길이가 좋은데, 너무 길면 FIFO로 변하고 짧으면 문맥 교환(Context Switching) 비용이 증가합니다.
2) 하드웨어적 타이머가 필요합니다.
3) 미완성 작업은 규정 시간량(시간 할당량)을 마친 후 프로세서를 기다리니까 평균 처리 시간이 높습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

 

 

 

 

빌드 자동화 툴 Cmake에 관한 설명과 사용법을 알아보도록 하겠습니다.

 

Cmake 사용하는 이유

Cmake는 Makefile을 만들어주는 툴입니다.

Cmake 설명하기 전에 Makefile 무엇이냐면 빌드를 편리하게 해주는 일종의 빌드 스크립트라고 보시면 됩니다. 스크립트를 make명령을 사용해서 실행시키기는 겁니다.

그냥 shell파일을 이용해서 빌드를 있겠지만 Makefile 사용하는 이유는 Incremental build 방식을 사용하기 때문입니다.

 

Incremental build?

프로젝트의 규모가 커질수록 수많은 소스 파일들이 생겨날테고,  빌드하는 시간도 오래걸리게 됩니다. 우리가 대학교에서 프로젝트하는 수준은 그냥 강아지 애교 수준이고 정말 회사에서 일하게 경우에는 빌드하는 것도 시간이 걸립니다.

그래서 우선 처음에는 모두 빌드를 해놓고, 이후 수정된 파일에 대해서는 소스파일과 연관된 것들만 빌드하여 시간을 줄여주는 빌드 방식이라고 생각하시면 됩니다.

 

이러한 편리성이 존재하지만 Make 의존성에 관한 기술을 일일이 기재해야 되기 때문에 관리측면에서 번거롭습니다.

Cmake 이런 귀찮은 점을 해결해주는 툴로서 Makefile 생성해내는 역할을 합니다.

 

 

간단하게 Cmake 사용하는 방법을 알아보도록 하겠습니다.

a.c

int add(int a,int b){

    return a+b;

}

 

a.h

int add(int,int);

 

b.c

#include <stdio.h>

#include "a.h"



int main(){

    printf("%d+%d=%d\n",5,6,add(5,6));

    return 0;

}

 

 

a.c add라는 함수가 존재하고 단지 수를 더해서 결과를 return하는 함수입니다.

a.h a.c 함수를 선언한 헤더파일이고 b.c a.h 함수를 사용하는 실제 프로그램입니다.

간단한 프로그램을 빌드하는 CMakeLists.txt 작성은 줄이면 됩니다.

 

 

 

 

 

 

CMakeLists.txt

 

ADD_EXECUTABLE(a.out a.c b.c)

 

ADD_EXECUTABLE 실행할 파일을 생성하는 명령으로 번째 기재할 내용은 실행파일 이름이고 나머지는 실행파일을 생성하기 위한 source파일입니다.

 

 

이제 아래의 명령을 통해서 실행해보도록 합시다.

cmake CMakeLists.txt

 


#cmake CMakeLists.txt

-- The C compiler identification is GNU 5.5.0

-- The CXX compiler identification is GNU 5.5.0

-- Check for working C compiler: /usr/bin/cc

-- Check for working C compiler: /usr/bin/cc -- works

-- Detecting C compiler ABI info

-- Detecting C compiler ABI info - done

-- Detecting C compile features

-- Detecting C compile features - done

-- Check for working CXX compiler: /usr/bin/c++

-- Check for working CXX compiler: /usr/bin/c++ -- works

-- Detecting CXX compiler ABI info

-- Detecting CXX compiler ABI info - done

-- Detecting CXX compile features

-- Detecting CXX compile features - done

-- Configuring done

-- Generating done

# ls

a.c  a.h  a.out  b.c  CMakeCache.txt  CMakeFiles  cmake_install.cmake  CMakeLists.txt  Makefile

 

그러면 소스파일 외에도 Cmake 관련된 파일이 생성되는데, Makefile 추가로 포함되었음을 있습니다.

만약 cmake 없었다면 Makefile 내용은 여러분들이 작성했어야하겠죠?

Cmake 이용하면 간단하게 줄이면 되니까 무척이나 편리합니다.

 

이제 실제 make 실행하면 우리가 만들어내고 싶은 a.out이라는 실행파일이 만들어집니다.


# make

[ 33%] Building C object CMakeFiles/a.out.dir/a.c.o

[ 66%] Building C object CMakeFiles/a.out.dir/b.c.o

[100%] Linking C executable a.out

[100%] Built target a.out

# ls

a.c  a.h  a.out  b.c  CMakeCache.txt  CMakeFiles  cmake_install.cmake  CMakeLists.txt  Makefile

# ./a.out

5+6=11

 

Cmake 변수 선언도 가능하며 다양한 명령어가 존재합니다. 이런 명령어들은 , 소문자를 구분하지 않지만 보편적으로 대문자를 사용합니다.

 

 

 

 

CMAKE_MINIMUM_REQUIRED()

Cmake빌드를 실행할 최소 버전을 명시합니다. 보통 위에 사용하며 기재된 버전보다 낮다면 오류를 보여주고 빌드를 종료합니다.

사용법은 아래와 같습니다.

CMAKE_MINIMUM_REQUIRED( VERSION 3.4.3 )

SET() 변수 정의

SET명령어는 변수를 정의하며 다음과 같이 사용됩니다.

SET(변수명 )

어렵지 않죠? 뿐만 아니라 List형식으로도 변수를 정의할 수도 있습니다.

SET(변수명 1 2 3)

이렇게 정의된 변수는 $변수, 혹는 "{}" 써서 ${변수} 사용할 있습니다.

앞의 CMakeLists.txt 변수를 통해 바꿔본다면 이렇게 바꿀 있습니다.

 

SET(SRC_FILES a.c b.c)

SET(BIN_NAME a.out)

ADD_EXECUTABLE(${BIN_NAME} ${SRC_FILES})

 

PROJECT() 프로젝트명 정의

PROJECT(프로젝트 )으로 쓰이게 되고 프로젝트의 이름을 정해주는 명령어로 CMAKE_PROJECT_NAME이라는 변수에 저장이 됩니다.

 

 

MESSAGE() 메시지 출력

혹시 변수에 들어있는 값이라던가 오류 메시지를 표시하려면 MESSAGE라는 명령어를 사용하여 출력하면 됩니다.

MESSAGE( TYPE MESSAGE)

메시지 종류는 생략가능하며 5가지의 종류가 있습니다. STATUS 가장 심각도가 약한 단계이고 FATAL_ERROR 심각도가 가장 심합니다.

 

TYPE 설명
STATUS 상태 메시지 출력, 계속 진행
WARNING 경고 메시지를 출력, 계속 진행
AUTHOR_WARNING 프로젝트 개발자용 경고 메시지를 출력, 계속 진행
SEND_ERROR 오류 메시지를 출력, 계속 진행, Makefile생성하지 않음
FATAL_ERROR 오류 메시지를 출력, 작업 중단

 

 

ADD_LIBRARY()

ADD_LIBRARY 실행파일이 아닌 library파일을 생성할 쓰이는 명령입니다. 아래와 같이 쓰이게 되죠.

ADD_LIBRARY( library이름 TYPE 파일1 파일2 …)

처음에는 라이브러리 이름이 쓰이고 두번째는 라이브러리의 종류를 나타냅니다. Default STATIC 쓰이며 아래와 같이 3개의 종류 하나를 지정할 있습니다.

STATIC, SHARED, MODULE

그리고 후에는 사용될 파일을 나열하면 됩니다.

 

위의 CMakeLists.txt 수정하여 소스파일 a 공유 라이브러리를 만든다면 아래와 같이 수정하시면 됩니다.

 

SET(SRC_FILES a.c)

SET(LIB_NAME a)

ADD_LIBRARY(${LIB_NAME} SHARED ${SRC_FILES})

 

후에 빌드하면 lib(라이브러리 이름).so 파일이 생기는 것을 확인할 있습니다

ADD_DEFINITIONS()

전처리시에 정의할 매크로를 의미합니다. 앞에 -D 쓰고 나서 매크로명을 써야하며 #define 구문의 역할을 한다고 보시면 됩니다. 2가지 사용법이 있습니다. 컴파일시에 -D매크로명 옵션과 같습니다.

ADD_DEFINITIONS(-D매크로명) : 단순 define으로 매크로를 정의할때 사용합니다.

ADD_DEFINITIONS(-D매크로명=) : 매크로에 값을 집어넣습니다.

 

INCLUDE_DIRECTORIES()

#include에서 사용할 디렉토리를 명시합니다. 만약 inc 디렉토리에 헤더파일이 존재하는 경우 아래와 같이 사용하면 됩니다.

INCLUDE_DIRECTORIES(inc)

 

이상 간단하게 CMake의 개념과 사용법을 알아보았습니다. 다음에는 더 심도있게 알아보도록 하겠습니다.

 

 

 

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

동기화, 임계 영역 등 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

교착상태(Deadlock)

deadlock... 이름도 뭣같이 무섭네요. 교착상태라고 하는 것은 다중 프로그래밍 시스템에서 프로세스 혹은 스레드가 결코 일어나지 않을 일을 무한정으로 기다리는 상태를 말합니다. 시스템에서 교착상태는 프로세스가 요구하는 자원이 엉켜서 더 이상 작업을 더 실행할 수 없는 상태를 의미합니다.

 

시스템이라고 생각하면 조금 어려울 수 있는데 인간 세계에서도 똑같아요. 예를 들어볼까요?

 


평화로운 중고 사이트에서 A가 사고 싶은 노트북이 생겨서 B에게 연락을 합니다. 가뜩이나 요즘 중고사기가 판을 치는 시기에 B에게 먼저 노트북을 택배로 보내면 150만원을 입금해주겠다고 합니다. B 역시 A를 의심하여 먼저 입금하면 노트북을 보내겠다고 합니다.
A는 B의 노트북을, B는 A의 돈을 요구하는 상황이고 서로 주지않습니다. 이렇게 서로 자원(돈, 노트북)을 점유한 상태에서 막혀 버려 서로 해야할 일(거래)을 하지 못하는 것입니다.
아무 것도 아닌 일 때문에 해야할 일을 못하는 것이지요. 어이가없네

 

교착상태의 발생 조건

아래 설명에서는 프로세스만을 이야기하는데, 스레드도 같습니다. 편의상 프로세스만 예를 들어 이야기하도록 하겠습니다.

 

1. 상호배제 (Mutual Exclusion)

한 번에  한 프로세스만 자원을 사용할 수 있어야합니다. 사용중인 자원을 다른 프로세스가 사용하려면 요청한 자원이 다 사용되고 난 후 해제될때까지 기다려야합니다. 

 

2. 점유와 대기 (Hold And Wait)

프로세스는 자원을 점유한 동시에 대기해야합니다. 위의 예에서 볼 수 있듯이 A는 돈을 소유하고 있고 B가 노트북을 주기까지 대기하고 있습니다. 

 

3. 비선점 (Non Preemptive)

자원을 선점할 수 없는 조건으로 누군가가 자원을 가지고 있을때 뺏을 수 없습니다. 위의 예에서도 지극히 정상적인 상식에서 A와 B는 물건을 불법적으로 훔칠 수 없죠. 

 

4. 순환(환형) 대기 (Circle wait)

프로세스가 여러개 있을때 자원의 점유와 요청의 관계가 순환되는 조건을 말합니다. 프로세스 3개를 각각 p1, p2, p3라고 할때 p1이 p2의 자원을 요청하고, p2가 p3의 자원을 요청하고, p3가 p1의 자원을 요청하고 그 자원은 요청한 자원을 얻을 수 있을때까지 반환할 수 없습니다. 이 조건은 위의 세 조건이 만족이 되면 자연스레 만족됩니다.

 

아래의 그림은 순환 대기를 나타낸 것인데 R1, R2, R3는 각 P1, P2, P3가 점유한 자원을 의미하고 각 자원은 소유된 프로세스쪽으로 화살표가 나가고 있습니다. 프로세스들의 화살표는 요구하는 자원쪽으로 향하고 있지요. 여기서 이 화살표들의 방향대로 나가면 사이클을 그리며 무한정 순환하게 됩니다.

 

 

 

 

 

교착상태 발생 코드

아래의 코드는 교착상태를 스레드를 이용해서 발생한 예제입니다. 여기서는 간단히 메인 스레드와 메인 스레드에서 생성된 스레드를 고려합니다.

여기서 자원은 뮤텍스인 lock1과 lock2를 말하지요. 

 

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>


pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;

void *function(void* arg){

        sleep(1);       //main thread가 먼저 실행할 여지를 줌
        printf("\t thread start\n");

        pthread_mutex_lock(&lock2);
        pthread_mutex_lock(&lock1);

        printf("\t critical section \n");

        pthread_mutex_unlock(&lock1);
        pthread_mutex_unlock(&lock2);

        printf("\t thread end\n");

}

int main(){
        pthread_t tid;

        printf("main thread start and create thread\n");
        pthread_create(&tid,NULL,function,NULL);

        pthread_mutex_lock(&lock1);
        sleep(5);               //thread가 lock2를 먼저 실행할 여유를 줌
        pthread_mutex_lock(&lock2);

        printf("critical section \n");

        pthread_mutex_unlock(&lock2);
        pthread_mutex_unlock(&lock1);

        pthread_join(tid,NULL);
        printf("main thread end\n");

}

 

(정확히 100% 아래처럼 동작한다고 말할 수는 없지만 위 코드는 대부분 제가 의도한 대로 동작합니다. 왜냐면 스레드 실행 순서의 흐름은 예측할 수 없으니까요.)

 

코드를 보면 각각 critical section을 pthread_mutex_lock을 통해 보호하고 있지요. 이 소스 코드에서 눈 여겨 봐야할 점은 메인 스레드가 우선 lock1을 점유하고, 이후 생성된 스레드는 lock2를 점유하고 있습니다. 

바로 메인 스레드는 lock2를 점유하여 임계 영역에 진입하여야하는데 이미 생성된 스레드가 가지고 있습니다. 그렇기 때문에 lock2가 해제될때까지 기다립니다.

한편 생성된 스레드는 lock2는 점유했고 lock1을 얻은 후에 임계 영역을 진입하려합니다. 하지만 lock1은 이미 메인 스레드에 의해 점유되었습니다. 따라서 lock1이 해제될때까지 기다려야합니다.

 

여기서 위의 네가지 조건이 보이시나요?

첫번째 조건으로 critical section으로 특정 한 스레드만 임계 영역에 진입할 수 있습니다. 여기서 임계 영역이 중첩되어 있네요. lock1을 이용해 잠근 임계영역과 lock2를 통한 임계영역이지요.

두번째 조건으로 점유와 대기를 이루고 있습니다. 메인 스레드는 lock1을 점유하고 lock2가 해제되기를 기다리고 있고 생성된 스레드는 lock2를 점유하고 메인 스레드에 의해 lock1이 해제될때까지 기다리고 있습니다.

세번째 조건은 비선점 조건인데 서로 강제로 lock1, lock2를 가져올 수 없습니다.

네번째 조건은 위의 그림처럼 그려보면 사이클을 이루게 되죠.

 

 

이 프로그램의 실행결과는 어떨까요? 무사히 수행이 될까요? 실행시켜 보도록 하겠습니다.

 


  # ./a.out
  main thread start and create thread
           thread start

네, 사용자가 중지하지 않는 이상 이 프로그램은 영원히 끝나지 않습니다. 아래에서 교착상태를 해결하는 코드를 볼 수 있습니다.

 

 

 

 

교착상태 문제 해결

교착상태가 발생하면 어떻게 해결할 수 있을까요? 크게 세가지 방법이 있는데 간략히 알아보도록 합시다.

 

1) 교착 상태 예방

교착 상태가 발생하기 전에 조치를 취하는 방식으로 위의 4가지 조건 중 하나라도 발생시키지 않으면 됩니다. 

- 자원의 상호배제 조건 방지

- 점유와 대기 조건 방지

- 비선점 조건 방지

- 순환 대기 조건 방지

 

앞서 얘기했듯이 처음 세조건이 만족되면 순환 대기가 발생하므로 결국에는 순환대기를 발생시키지 않으면 됩니다.

교착 상태 예방은 시스템의 처리량을 떨어뜨립니다. 

 

2) 교착 상태 회피

예방처럼 교착 상태의 발생 가능성을 미리 제거하지 않고 교착상태가 일어난다고 보고 이것을 회피하는 것입니다. 예를 들어 몇 초동안 프로세스나 스레드가 임계 구역 전에 대기 중이라면 이 시간이 지난 후 다음 작업을 수행합니다. 위의 예제코드를 아래와 같이 수정할 수 있습니다.

 

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>


pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;

void *function(void* arg){
        struct timespec tout;
        struct tm* t;
        int locked;

        sleep(1);       //main thread가 먼저 실행할 여지를 줌
        printf("\t thread start\n");


        clock_gettime(CLOCK_REALTIME,&tout);
        tout.tv_sec += 5;

        pthread_mutex_lock(&lock2);
        locked = pthread_mutex_timedlock(&lock1,&tout);  //5초간만 lock이 걸림

        printf("\t critical section \n");

        if(locked == 0){
                pthread_mutex_unlock(&lock1);
        }
        pthread_mutex_unlock(&lock2);

        printf("\t thread end\n");

}

int main(){
        pthread_t tid;

        printf("main thread start and create thread\n");
        pthread_create(&tid,NULL,function,NULL);

        pthread_mutex_lock(&lock1);
        sleep(5);               //thread가 lock2를 먼저 실행할 여유를 줌
        pthread_mutex_lock(&lock2);

        printf("critical section \n");

        pthread_mutex_unlock(&lock2);
        pthread_mutex_unlock(&lock1);

        pthread_join(tid,NULL);
        printf("main thread end\n");

}

 

pthread_mutex_timed_lock 함수는 특정 시간동안 lock을 얻을 수 없다면 뮤텍스를 잠그지 않고 ETIMEDOUT이라는 오류 부호를 돌려줍니다. 따라서 스레드가 무한정 차단되지 않게 만듭니다. 이 코드를 실행시켜보면 아래와 같이 끝나기는 합니다.

 


# ./a.out
main thread start and create thread
         thread start
         critical section
critical section
         thread end
main thread end

 

 

3) 교착 상태 회복 

교착 상태를 시스템에서 탐지하여 회복시키는 알고리즘으로 교착상태를 회복합니다.

 

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

 

 

아래와 같이 caculator라는 프로그램이 같은 디렉토리에 있는 lib내의 libcalcops라는 라이브러리를 사용할 수 있도록 만들어보도록 하겠습니다.

 

아주 간략하게 무슨 프로그램이냐를 설명해드리면, calculator라는 이 프로그램은 사용자로부터 두 수를 입력받아 덧셈, 뺄셈, 나눗셈의 결과를 출력해주는 프로그램입니다.

우리는 더 쉽게 calculator라는 프로그램을 구현하기 위해서(사실은 그냥 짜는게 더 쉽지만 라이브러리를 연동하는 방법을 알기 위해서) lib/libcalcops의 라이브러리를 가져다가 쓸겁니다. 물론 우리가 만들어 쓸거에요.

이 라이브러리에는 실제 연산 동작(calc operation)이 구현되어있습니다.

 

 

우선 현재 디렉토리에 mkdir lib를 이용해서 lib를 저장할 수 있는 디렉토리를 만들어줍시다.

 


# mkdir lib
# cd lib

 

라이브러리 소스코드 작성

너무나 간단한 연산 동작을 하는 함수를 정의한 헤더파일과 구현한 c파일을 작성하고 컴파일 해줍니다.

 

calc_operations.h

#ifndef __CALC_OPERATION_H_
#define __CALC_OPERATION_H_
int add(int a,int b);
int sub(int a,int b);
int mult(int a,int b);
double div(int a,int b);
#endif

 

calc_operations.c

#include "calc_operations.h"

int add(int a,int b){
    return a+b;
}

int sub(int a,int b){
    return a-b;
}

int mult(int a,int b){
    return a*b;
}

double div(int a,int b){
    return (double)a/(double)b;
}

 

여기까지는 뭐 누구나 아는 프로그램이지요. 아래와 같이 컴파일하게 되면 object파일이 하나 생성되겠죠.


# gcc -c calc_operations.c
# ls
calc_operations.c  calc_operations.h  calc_operations.o 

 

이제 이것을 ar로 rc옵션을 주어 정적라이브러리 아카이브를 만듭니다. 


# ar rc libcalcops.a calc_operations.o
# ls
calc_operations.c  calc_operations.h  calc_operations.o  libcalcops.a

 

프로그램 구현

이제 lib밖으로(cd ..) 이동해주세요. 여기서 소스 코드를 구현하도록 하겠습니다.

 

calculator.c

#include <stdio.h>
#include "lib/calc_operations.h"

int main(){
        int a,b;
        scanf("%d %d",&a,&b);

        printf("%d + %d = %d\n", a,b,add(a,b));
        printf("%d - %d = %d\n", a,b,sub(a,b));
        printf("%d * %d = %d\n", a,b,mult(a,b));
        printf("%d / %d = %f\n", a,b,div(a,b));

}

 

이제 아래와 같이 컴파일하도록 해봅시다. 아래 옵션 중에서 -L은 라이브러리가 위치한 디렉토리를 명시해주고 -lcalcops는 우리가 작성한 lib파일 이름입니다. 우리가 lib파일 이름을 libcalcops.a라고 정의하였을때 옵션은 -lcalcops라고 명시해주면 됩니다. lib가 -l로 바뀌고 확장자 .a를 빼주는 식이죠.


# gcc calculator.c -L ./lib -lcalcops
# ./a.out
7 3
7 + 3 = 10
7 - 3 = 4
7 * 3 = 21
7 / 3 = 2.333333

기존의 .o파일과 .a를 지우고 나서도 calculator 프로그램이 동작할까요? 지우고 실행을 다시 해보도록 합시다.


# rm -rf *.a *.o
# ls
calc_operations.c  calc_operations.h
# cd ..
# # ./calculator
10 3
10 + 3 = 13
10 - 3 = 7
10 * 3 = 30
10 / 3 = 3.333333

보시다시피 잘 동작하는 것을 알 수 있습니다. 추측해볼 수 있는 것은 런타임과는 관계가 없고 컴파일시(calculator)에 프로그램에 적재되는 것을 알 수 있고 실제로도 그렇습니다.

 

 

 

공유라이브러리

공유라이브러리는 컴파일 방식만 변경하면 됩니다. lib 디렉토리로 이동해주세요. 

아래의 명령으로 라이브러리를 컴파일해줍니다. 그런후에 /usr/lib 디렉토리에 복사해줍니다.


 # gcc -shared -o libcalcops.so calc_operations.c
 # cp libcalcops.so /usr/lib

 

이제 calculator 프로그램을 컴파일 할것인데, 위의 컴파일 옵션보다 더 간단합니다.


# gcc calculator.c -lcalcops
# ./a.out
8 3
8 + 3 = 11
8 - 3 = 5
8 * 3 = 24
8 / 3 = 2.666667

 

그렇다면 아까와 같이 /usr/lib/libcalcops.so파일을 삭제하고 위 프로그램을 다시 실행해보도록 해봅시다.


# ./a.out
./a.out: error while loading shared libraries: libcalcops.so: cannot open shared object file: No such file or directory

 

정적라이브러리와는 다르게 라이브러리 파일이 삭제되면 실행 프로그램도 실행되지 않네요. 우리가 추측해볼 수 있는것은 프로그램이 시작시에 적재되는 것을 의심해볼 수 있고 실제로도 그렇습니다.

 

왜 하필 /usr/lib에 파일을 복사했을까요? 공유라이브러리할때 /etc/ld.so.conf 설정파일로부터 라이브러리 파일이 있는 지 봅니다. 

제 시스템을 기준으로 설명드리면 아래와 같이 /etc/ld.so.conf 파일이 작성되어 있습니다.


include /etc/ld.so.conf.d/*.conf

 

다시 /etc/ld.so.conf.d/ 디렉토리를 보면 libc.conf와 x86_64-linux-gnu.conf 파일 두개가 존재합니다. 하기의 경로에 lib파일이 있으면 실행되며 추가적으로 경로를 추가 후 저장하고 나와서 ldconfig 명령을 주어 반영하면 됩니다.

libc.conf x86_64-linux-gnu.conf
# libc default configuration
/usr/local/lib
/usr/lib
# Multiarch support
/usr/local/lib/x86_64-linux-gnu
/lib/x86_64-linux-gnu
/usr/lib/x86_64-linux-gnu

 

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,