[ 쉽게 배우는 운영체제 ] 5-4. 파일, 파이프, 소켓 프로그래밍
파일
순차파일
파일 내의 데이터는 한 줄로 길게 저장되는데 이러한 파일을 순차파일(sequential file)이라고 하고, 순차파일에 접근하는 방식을 순차적 접근(sequential access)이라 한다.
순차적 접근의 대표적인 예로는 카세트테이프를 들 수 있다.
파일 기술자
파일을 포함하여 모든 통신에 관련된 연산은 open(), read()/write(), close() 구조다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#include <studio.h>
#include <unistd.h>
#include <fcntl.h>
void main()
{ int fd;
char buf[5];
fd=open("com.txt", O_RDWR); // com.txt를 읽기/쓰기 전용으로 연다
// * O_RDWR : 파일을 읽기/쓰기용으로 연다.
read(fd, buf, 5); // 파일에서 5번째 파일 기술자의 위치를 읽어 해당 값을 변수 buf에 저장
printf("%s", buf);
close(fd);
return 0;
}
|
cs |
open() : 열고자 하려는 파일이 있는지, 그 파일이 있다면 접근권한이 있는지, 파일을 어떤 방식으로 열 것인지를 결정한다. 파일을 여는 방식에는 읽기 전용(read only), 읽기/쓰기(read/write), 쓰기 전용(write only), 생성(create)등이 있다.
* O_RDWR : 파일을 읽기/쓰기용으로 연다.
파일 기술자는 파일 접근 권한 외에 현재 파일의 어느 위치를 읽고 있는지에 대한 정보도 보관한다.
처음 파일이 열리면 파일 기술자는 맨 앞에 위치한다. 파일에서 파일 기술자는 단 하나이고, 읽기를 하든 쓰기를 하든 파일 기술자는 계속 전진한다.
파일을 다 사용하고 close() 함수를 사용하면 파일이 닫힌다.
파일을 이용한 통신
부모 프로세스와 자식 프로세스가 파일을 이용하여 통신을 하는 코드이다.
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 <studio.h>
#include <unistd.h>
#include <fcntl.h>
void main()
{ int pid, fd;
char buf[5];
fd=open("com.txt", O_RDWR); /* init */
// fork()문을 실행하기 전에 파일을 open()한다.(= 생성된 파일 기술자가 자식 프로세스에도 상속된다)
pid=fork(); //fork
if(pid<0 || fd<0) exit(-1);
else if(pid==0) { /* child */
write(fd, "Test", 5); /* 파일 com.txt에 Test라고 쓴다.
여기서 Test는 4B이지만 널문자(\0)이 필요하기때문에 5B할당한다. */
close(fd); // 파일 기술자 닫고 종료
exit(0); }
else { wait(0); /* parent */
lseek(fd, 0, SEEK_SET);
read(fd, buf, 5);
printf("%s", buf);
close(fd);
exit(0); }
}
|
cs |
부모 프로세스가 자식 프로세스보다 먼저 실행되면 자식 프로세스가 아무 작업도 하지 않았기 때문에 빈 공간을 읽게 된다.
따라서 부모 프로세스와 자식 프로세스 간에 동기화를 해주어야 한다.
부모 프로세스는 자식 프로세스의 작업이 끝난 후 read()를 수행해야 하기 때문에 wait()를 사용하여 자식 프로세스를 기다린다.
자식 프로세스가 작업을 마치면 lseek()가 실행되어 파일 기술자가 맨 앞으로 옮겨진다. 그 후 읽은 데이터를 화면에 출력하며 파일 기술자를 닫고 프로그램을 끝낸다.
파일 기술자(file descriptor; fd)는 해당 파일에 접근할 수 있는 권리로 이 변수를 통해서만 파일 읽기/쓰기에 접근할 수 있다.
fork()는 부모 프로세스의 대부분을 자식 프로세스에도 그대로 복사해주는데 파일 기술자도 이에 포함된다. 파일 기술자가 자식 프로세스에도 복사되므로 부모와 자식 프로세스 모두 com.txt를 읽거나 쓸 수 있다.
이처럼 파일 기술자가 자식 프로세스에도 복사되기 때문에 open()은 한 번(init)이지만 close()는 자식 프로세스에서 한번, 부모 프로세스에서 한번 두 번 발생한다.
lseek()는 파일 기술자 fd를 임의로 움직이는 명령어로 움직이는 기준점을 정할 수 있다.
- SEEK_SET : 파일의 맨 처음 위치
- SEEK_CUR : 파일 기술자의 현재 위치
- SEEK_END : 파일의 매 마지막 위치가 기준이다.
파이프
동기화를 지원하는 단방향 통신 시스템으로, 이름 없는 파이프와 이름 있는 파이프로 나뉜다. 일반적으로 파이프는 이름 없는 파이프를 말한다. 파이프는 부모와 자식 프로세스 혹은 같은 부모의 자식 프로세스처럼 서로 관련 있는 프로세스 간 통신에 사용된다.
파일의 경우와 마찬가지로 파이프도 기술자를 초기화한 후 읽거나 쓰기 연산을 하고 close()로 기술자를 닫는 구조이다.
파이프는 파일 기술자를 2개의 원소를 가진 배열로 정의하는데, 하나는 읽기용이고 하나는 쓰기용이다. 이렇게 두 기술자가 따로 존재하기 때문에 동기화가 가능하다.
fd[2]를 선언한 후 fork() 시 총 4개의 파일 기술자가 존재하게 된다. 따라서 close()도 4개 존재한다.
파이프는 단방향 통신이기 때문에 프로세스 당 하나의 파일 기술자만 사용한다. 따라서 필요 없는 파일 기술자는 닫아버린다.
위 코드에서는 자식 프로세스가 fd[1]에 쓰고 부모 프로세스가 fd[0]으로 받는 구조이므로, 자식 프로세스는 사용하지 않을 fd[0]을 닫고 부모 프로세스는 fd[1]을 닫는다.
파이프 기술자가 4개이기 때문에 파이프 기술자를 닫을 것이 아니라 양방향 통신에 이용하면 된다고 생각할 수도 있지만, 자신이 write 한 것을 자신이 read 한 것이기 때문에 부모 프로세스에 쓰고자 한 데이터가 전달되지 않는다.
파이프로 양방향 통신을 구현하려면 파이프를 2개 사용해야 한다.
파이프의 또 다른 특징으로는 부모 프로세스에 wait()이 없다는 것이다. wait()리 없으면 부모 프로세스와 자식 프로세스 중 어떤 프로세스가 먼저 실행되는지 보장할 수 없지만 파이프는 대기가 있는 통신이기 때문에 wait()가 필요 없다.
네트워킹
여러 컴퓨터에 있는 프로세스에 데이터를 전달하는 방법 중 가장 대중화된 방식으로 소켓을 이용한 네트워킹에도 open(), read()/write(), close() 구조를 사용한다.
클라이언트와 서버는 둘 다 소켓을 사용하며, 소켓은 파이프와 달리 양방향 통신을 지원하고 동기화도 지원한다.
클라이언트
1. 소켓을 생성한다.
2. connect()를 사용하여 서버와의 접속을 시도한다.
3. 서버와 접속되면 read() / write() 작업 수행한다.
4. 작업이 끝나면 사용한 소켓 기술자(socket descriptor)를 닫고 종료한다.
서버
1. 서버는 소켓을 생성한다.
2. bind()를 사용하여 생성한 소켓을 특정 포트에 등록한다.
여러 컴퓨터가 연결된 네트워크 환경에서는 각 컴퓨터를 IP주소로 구분하는데 한 컴퓨터 내에도 여러 프로세스가 존재하기 때문에 어떤 프로세스와 통신할지 구분해야 한다.
이때 사용하는 구분 번호를 포트번호(port number)라고 한다.
하나의 포트번호에 소켓이 하나만 생성될 경우 한 웹페이지에 한 사람만 접속이 가능하다는 의미이므로, 서버는 동시에 여러 클라이언트에 서비스를 하기 위해 하나의 포트번호에 여러 개의 소켓을 생성한다.
3. listen()을 실행하여 클라이언트를 받을 준비를 한다.
4. accept()를 이용해 클라이언트의 connection(연결 요청)을 기다리다가 여러 명의 클라이언트가 동시에 connect()하는 경우 그중 하나를 골라 작업을 시작하게 한다.
5. send()/write() 작업을 실행한다.
6. 생성된 소켓 기술자를 닫고 다음 클라이언트를 기다린다.
클라이언트 코드
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 <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
void main()
{ int sp;
char buf[5];
struct sockaddr_in ad; // 주소와 관련된 정보
sp=socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // socket()구문으로 소켓 생성, sp변수로 이 소켓에 접근
/* 통신 초기화 */
memset(&ad, 0, sizeof(ad));
ad.sin_family=AF_INET; // AF_INET : IPv4 인터넷 프로토콜
ad.sin_addr.s_addr=inet_addr("127.0.0.1"); // 127.0.0.1 : 루프백 주소
ad.sin_port=htons(11234);
connect(sp, (struct sockaddr *) &ad, sizeof(ad)); // 통신 초기화시 connect()시도
read(sp, buf, 5);
printf("%s", buf); // 연결시 소켓으로부터 5B읽고 화면에 출력
close(sp);
exit(0);
}
|
cs |
socket() 구문으로 소켓을 생성하고 변수 sp로 이 소켓에 접근한다.
socket() 아래 4줄은 통신을 초기화하는 부분이며 변수 ad가 주소와 관련된 정보를 가지게 된다.
이 코드에서 사용한 IP주소는 루프백 주소이며, 서버의 포트번호는 임의의 값인 11234이다.
통신이 초기화되면 sp와 ad를 이용하여 서버와 connect()를 시도한다.
연결이 이루어지면 소켓으로부터 5B를 읽어 화면에 출력하고 사용한 소켓 기술자를 닫은 후 클라이언트 프로그램을 끝낸다.
* 루프백 주소
IP주소 127.0.0.1을 말하며 localhost라고도 하는 특수 목적의 IPv4 주소이다. 인터넷이 연결되어있지 않아도 사용 가능하며 모든 컴퓨터는 이 주소를 자체 주소로 사용하지만 실제 IP주소처럼 다른 장치와 통신할 수 없다.
서버 코드
클라이언트 코드와 짝을 이루는 코드로 통신 초기화 기능은 거의 비슷하지만 서버에서는 클라이언트의 주소를 알 수 없기 때문에 htonl(INADDR_ANY)라고 지정했다.
* INADDR_ANY
자동으로 이 컴퓨터에 존재하는 랜카드 중 사용 가능한 랜카드의 IP주소를 사용하라는 의미로, 여러 ip주소에 들어오는 데이터를 모두 수신한다.
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 <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
void main()
{ int sp, sa;
char buf[5];
struct sockaddr_in ad;
/* 통신 초기화 */
memset(&ad, 0, sizeof(ad));
ad.sin_family=AF_INET; // AF_INET : IPv4 인터넷 프로토콜
ad.sin_addr.s_addr=htonl(INADDR_ANY);
ad.sin_port=htons(11234);
sp=socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // socket()구문으로 소켓 생성
bind(sp, (struct sockaddr *) $ad, sizeof(ad)); // bind()를 이용해 소켓 등록
listen(sp, 10); // listen()을 통해 클라이언트의 접속 확인
while(1) {
sa=accept(sp, 0, 0);
write(sa, "Test", 5); // sp대신 sa를 소켓 기술자로
close(sa);
}
}
|
cs |
통신을 초기화한 후 소켓을 생성하고 bind()를 이용해 소켓을 등록한다.
클라이언트의 요청이 언제 들어올지 알 수 없기 때문에 서버에서의 소켓 생성은 listen()으로 클라이언트의 접속을 확인한 후 accept()에서 이루어진다.
그러므로 write작업을 하려면 sp대신 sa를 소켓 기술자로 사용해야 한다.
서버의 경우 계속 클라이언트를 받아 작업해야 하기 때문에 작업이 끝나면 소켓 기술자를 닫고 무한 루프를 돈다.
여러 클라이언트가 접속하더라도 계속 Test를 클라이언트에 전송할 수 있다.
[ 참고 링크 ]