19. System Dependencies



Q 19.1
키보드에서 RETURN 키를 누르지 않고 바로 한 글자를 읽으려면 어떻게 해야 하나요? 또 키를 누를때 스크린에 그 문자가 찍히지 않게 하려면 어떻게 하죠?

Answer
이 문제에 대한 표준, 또는 이식성이 뛰어난 방법은 없습니다. screen과 keyboard에 관한 것은 아예 C 표준에 한마디도 나와 있지 않습니다. 대신 간단한 I/O (문자로 이루어진) 스트림(stream)에 대한 것만 나와 있습니다.

Interactive한 키보드 입력은 대개 여러 입력이 모아져서 프로그램에 한 줄씩 전달되게 됩니다. 이런 까닭은 운영 체제가 입력된 줄을 편집할 (backspace/delete 등) 수 있는 일관된 방법을 (편집 기능을 일일히 코딩하지 않더라도) 제공하기 위해서 입니다. 즉 사용자가 RETURN 키를 눌렀을 때에 비로소, 한 줄이 프로그램에 전달됩니다. 프로그램이 한 문자씩 읽어들이게 짜여져 있다고 하더라도 한 문자를 읽어들이는 함수(예를 들면 getchar())가 호출될 때, 프로그램이 중단되고, 사용자에게 한 줄을 입력받은 다음, 각각의 문자가 빠른 속도로 그 함수에게 전달됩니다.

질문한 내용처럼 한 문자가 읽혀지는 즉시, 프로그램에 전달되게 하려면. 한 줄씩 전달되게 하는 그 logic을 멈추게 해야 합니다. 어떤 시스템 (예를 들면 MS-DOS, VMS의 어떤 모드)에서는 프로그램이 OS 수준의 입력 함수를 불러서 해결하기도 하며, 어떤 시스템 (예를 들면 Unix, VMS의 어떤 모드)에서는 입력을 처리하는 (대개 “터미널 드라이버”라고 부르는) OS의 부분에다 줄 단위 처리 기능을 끄라고 알려야 합니다. 그리고 일반적인 입력 함수(예를 들면 read(), getchar(), 등)를 써서 한 문자씩 읽습니다. 또 어떤 시스템에서는 (특히 오래된 배치 프로세싱19.1 시스템) 입력 처리가 주변 장치에 의해 처리되며, 줄 단위 입력만 처리할 수 있어, 다른 방식을 쓸 수 없는 경우도 있습니다.

그러므로, 문자 단위로 처리하고 싶거나 (키보드 에코 기능을 끄고 싶다면), 여러분이 쓰고 있는 시스템에 의존적인 특수한 방법을 써야 합니다. comp.lang.c는 C 언어가 정의하고 있는 기능에 대한 것을 다루는 곳이므로 이 질문을 다루기에는 적당하지 않습니다. 시스템에 의존적인 다른 뉴스 그룹, 예를 들면 아래 뉴스 그룹에 묻는 것이 좋습니다:

또는 그러한 그룹의 FAQ 목록을 보는 것도 좋은 방법입니다. 또 이런 방법은 시스템의 여러 종류마다 각각 차이가 있을 수 있으므로, 참을성을 가지고 기다려야 할 지도 모릅니다.

그러나 이러한 질문은 자주 이 곳에 게시되므로, 일반적인 경우에 처리할 수 있는 방법을 간단히 소개합니다.

어떤 버전의 curses에서는 cbreak(), noecho(), getch()와 같은, 질문의 목적에 맞는 함수를 제공합니다. 간단히, 짧은 암호를 입력할 수 있는 루틴이 필요하다면 getpass()를 쓰면 됩니다.

UNIX에서는 ioctl() 함수를 써서 터미널 드라이버 모드를 제어할 수 있습니다. (“classic” 버전에서는 CBREAKRAW를 쓰고, System V나 POSIX 시스템에서는 ICANON, c_cc[VMIN], 그리고 c_cc[VTIME]을 쓰고, 대부분 시스템에서는 ECHO를 쓸 수 있습니다. 또 system()과 stty 명령도 쓸 수 있습니다. (좀 더 자세한 설명을 원한다면, “classic” 버전에서는 <sgtty.h>와 tty(4)를, System V에서는 <termio.h>와 termio(4)를, POSIX 시스템에서는 <termios.h>와 termios(4)를 찾아보시기 바랍니다.)

MS-DOS에서는 getch()getche()를 쓰거나, BIOS 인터럽트에 해당하는 함수를 쓰면 됩니다.

VMS에서는 스크린 관리(Screen Management) 루틴(SMG$)이나 curses, 또는 IO$_READVBLK 함수를 통해, 저수준 $QIO를 (또는 IO$M_NOECHO) 쓰면 됩니다. (VMS 터미널 드라이버에서는 한 번에 한 글자씩19.2 읽거나 “pass through” 모드를 쓸 수 있습니다.)

다른 운영체제에서 하는 방법은 직접 찾아보시기 바랍니다.

(덧붙이면, 단순히 setbuf()setvbuf()를 써서 stdin이 버퍼링되지 않게 하는 것은 한 번에 한 글자씩 받아 들이는 것과 별 상관이 없습니다.)

이식성이 뛰어난 프로그램을 만들고자 할 때 가장 좋은 방법은 이러한 함수들을 정의하고 (1) 터미널 드라이버나 입력 시스템을 “한 번에 한 글자씩” 읽도록 설정하고, (2) 문자를 입력받고, (3) 프로그램을 끝낼 때 원래의 터미널 모드로 복원시키는 것입니다. (이론상, 이런 종류의 함수들은 언젠가 C 표준에 포함될 것입니다.) 이 FAQ 목록의 확장판(질문 [*]20.40 참고)에서는 여러 시스템에서 이러한 함수를 만드는 법에 대한 예제가 나와 있습니다.

... TODO ... 덧붙여 질문 [*]19.2도 참고하시기 바랍니다.

Note
ioctl()을 쓰는 가장 주된 이유는 Terminal I/O를 제어하기 위한 것이며, [Lewine] 에 의하면 POSIX.1에서 Terminal I/O 인터페이스를 새로 만든 이유를 다음과 같이 설명하고 있습니다:

즉, ioctl()은 권장되지 않는 함수입니다. 자세한 것은 [Lewine] 를 참고하기 바랍니다.

References
[PCS] § 10 pp. 128-9, § 10.1 pp. 130-1
[POSIX] § 7.
[Lewine] Chap. 8 pp. 145



Q 19.2
문자를 읽어들이기 전에 얼마나 많은 문자가 대기하고 있는지 알아낼 방법이 있나요? 또 대기하고 있는 문자가 없을 경우, 바로 함수가 종료하게 만들 수 없을까요?

Answer
이 질문도, 마찬가지로 완전히 운영체제에 의존적인 부분입니다. 어떤 버전의 curses는 nodelay() 함수를 쓸 수 있게 해 줍니다. 시스템에 따라 다르지만 “nonblocking I/O”를 쓸 수 있는 시스템이 있습니다. 또는 “select”나 “poll”이라는 시스템 콜로 이 작업을 대신할 수 있습니다. 또 FIONREAD ioctl, c_cc[VTIME], kbhit(), rdchk(), 그리고 open()이나 fcntl()에서 O_NDELAY 옵션이 이러한 작업을 해 줄 수 있습니다. 덧붙여 질문 [*]19.1도 참고하시기 바랍니다.



Q 19.3
현재 어떤 일이 몇 퍼센트가 진행되었는지 계속 표시할 수 있는 방법이나 “twirling baton19.3”을 표시할 수 있는 방법을 알려주세요.

Answer
가장 간단하고, 이식성이 뛰어난 방법은 한 줄을 출력한 다음, 캐리지 리턴(carriage return) 문자인 `\r'을 출력하는 것입니다. 이 문자를 라인 피드(line feed) 문자 없이 혼자 쓰면 현재 줄을 다시 덮어 쓸 수 있습니다. 또는 백스페이스(backspace) 문자인 `\b'을 쓰면 커서를 한 칸 왼쪽으로 옮겨 한 글자를 덮어 쓸 수 있게 됩니다.

References
[C89] § 5.2.2



Q 19.4
화면을 지우는 방법은? 색깔을 입혀 텍스트를 출력하는 방법은? 커서를 지정한 x, y 위치로 옮길 수 있는 방법은?
Answer
이러한 방법은 여러분이 쓰고 있는 터미널(또는 디스플레이)의 종류에 따라 다릅니다. 대개는 termcap, terminfo, curses와 같은 라이브러리를 써서 이런 작업을 처리합니다. MS-DOS 시스템에서는 clrscr(), gotoxy()를 찾아보기 바랍니다.

화면을 지우는 방법 중 가장 이식성이 높은 방법은 폼 피드(form-feed) 문자 (`\f')를 출력하는 것입니다. 이 문자를 출력하면 대다수 화면이 지워지게 됩니다. (조금 지저분하지만) 더 이식성이 높은 방법은 충분히 많은 newline 문자를 출력해서 한 화면 분의 여러 줄들을 넘겨 버리는 것입니다. 마지막 수단으로, system()을 써서 (질문 [*]19.27 참고) 화면을 지우는 명령을 실행할 수도 있습니다.

References
[PCS] § 5.1.4 pp. 54-60, § 5.1.5 pp. 60-62



Q 19.5
화살표 키를 읽으려면 어떻게 해야 하나요?

Answer
Terminfo, 또 몇몇 termcap, 그리고 몇몇 curses는 ASCII가 아닌 키들을 입력받을 수 있습니다. 일반적으로 이러한 키들은 여러 개의 문자 입력으로 (대개 첫번째 문자는 ESC, `\033') 처리되며, 이 입력을 분석해 내는 것은 상당히 까다롭습니다. (keypad()를 부를 경우 curses가 이 작업을 대신 처리해 줍니다.)

MS-DOS에서는 만약 문자 값이 0인 것(문자 `0'이 아님!)이 들어온다는 것은, 다음 문자가 특별한 키 입력 값이라는 것을 나타내는 신호입니다. 이 것을 스캔 코드(scan code)라고 하는데, 여기에 관한 것은 여러 DOS programming guide를 찾아보시면 됩니다. (몇 개만 말해보면; up, left, right, down 화살표 키는 각각 72, 75, 77, 80이며, function key들은 59에서 68 사이의 값을 가집니다.)

References
[PCS] § 5.1.4 pp. 56-7



Q 19.6
마우스 입력은 어떻게 처리하나요?
Answer
시스템 문서들을 읽어보거나 적당한 뉴스 그룹에 물어보시기 바랍니다. (뉴스 그룹에 물어볼 때에는 먼저 FAQ 목록을 본 다음에) 마우스 처리는 X 윈도우 시스템, MS-DOS, 매킨토시 등등의 시스템에 따라 매우 다릅니다.

References
[PCS] § 5.5 pp. 78-80



Q 19.7
시리얼(“comm”) 포트 입/출력은 어떻게 처리하죠?

Answer
마찬가지로 시스템에 의존적인 문제입니다. UNIX에서는 /dev에 있는 적당한 장치 파일을 open, read, write 함수를, 터미널 드라이버가 제공하는 여러 기능들과 함께 쓰면 됩니다. MS-DOS에서는 미리 정의된 스트림인 stdaux를 쓰거나 “COM1”과 같은 이름으로 파일을 열거나 저수준 BIOS 인터럽트들을 쓰면 됩니다. 많은 네터19.4들이 Joe Campbell씨의 C Programmer's Guide to Serial Communications을 추천하고 있습니다.



Q 19.8
출력을 프린터로 보내는 방법을 알려주세요.
Answer
Unix에서는 popen() (질문 [*]19.30 참고) 함수를 써서 `lp'나 `lpr' 프로그램을 실행하거나, `/dev/lp'와 같은 특수 파일을 열어서 작업합니다. MS-DOS에서는 (비표준) 미리 정의된 스트림인 stdprn을 쓰거나 “PRN”이나 “LPT1”으로 파일을 열어서 작업하면 됩니다.
References
[PCS] § 5.3 pp. 72-74



Q 19.9
Escape sequence를 터미널이나 비슷한 장치에 보내는 방법은?

Answer
문자를 장치에 보내는 방법을 알고 있다면 (질문 [*]19.8 참고), Escape sequence를 보내는 방법은 매우 쉽습니다. ASCII로 ESC는 033, (10 진수로 27), 이므로:

  fprintf(ofd, "\033[J");
을 쓰면 escape sequence, ESC [ J를 보낼 수 있습니다.



Q 19.10
그래픽을 처리하는 방법은?
Answer
옛날, Unix는, plot(3)와 plot(5)에서 설명하는, 장치 독립적인 plot 함수를 제공했습니다. GNU libplot 패키지는 같은 목적으로 좀 더 나은 기능을 제공합니다. 다음 URL을 참고하기 바랍니다:

  http://www.gnu.org/software/plotutils/plotutils.html

MS-DOS에서 작업한다면 VESA나 BGI 표준에 따르는 라이브러리를 쓰기를 원할 것입니다.

플로터에 그리는 작업은 대개 특정 escape sequence로 이루어집니다; 플로터 제조자는 대개 C 언어로 된 라이브러리 패키지를 제공하므로, 이 것을 쓰던지 net을 뒤져보기 바랍니다.

윈도우 시스템(매킨토시나 X Window System, 또는 Microsoft Windows)을 쓰고 있다면, 윈도우 기능을 쓰고 싶어할지도 모릅니다; 이 경우 관련된 뉴스 그룹이나 FAQ 목록을 먼저 참고하기 바랍니다.

References
[PCS] § 5.4 pp. 75-77



Q 19.11
저는 사용자에게 입력 파일이 없다는 경고를 출력하고 싶습니다. 파일이 존재하는 지 어떻게 하면 검사할 수 있을까요?
Answer
이런 간단한 문제도 표준에 맞게, 또는 호환성이 높게 처리할 방법이 없다는 것은 참으로 안타까울 뿐입니다. 검사하는 어떤 방법을 썼다고 하더라도 테스트 한 후, 파일을 열기 전에 (다른 프로세스에 의해) 그 파일이 새로 만들어지거나 지워질 수 있기 때문입니다.

이런 목적으로 쓸 수 있는 함수는 stat(), access(), fopen()이 있습니다. (fopen()을 쓴다면 파일을 읽기 모드로 열고 바로 닫으면 됩니다. fopen()이 실패한다고 해서 무조건 파일이 존재하지 않는다는 것은 아닙니다.) 물론 이 함수들 중에서는 fopen()이 가장 이식성이 뛰어납니다. UNIX의 set-UID 기능이 있다면 access() 함수는 주의깊게 써야 합니다.

단순히 파일이 성공적으로 열렸다고 가정하는 것보다 항상 리턴 값을 검사해서 실패했는지 조사하는 것이 바람직합니다.

References
[PCS] § 12 pp. 189, 213
[POSIX] § 5.3.1, § 5.6.2, § 5.6.3



Q 19.12
파일을 읽기 전에 그 파일의 크기를 알 수 있을까요?
Answer
C 언어에서 “파일의 크기”란 파일에서 읽을 수 있는 문자의 갯수를 말합니다. 정확히 그 크기를 아는 것은 매우 어렵거나 불가능합니다.

UNIX에서는 stat() 함수가 정확한 값을 알려 줄 수 있습니다. 대부분의 다른 시스템에서는 UNIX와 비슷한 stat() 함수를 제공하고, 비슷한 값(정확하지 않을 수도 있음)을 알려줍니다. fseek()를 써서 파일 위치를 맨 뒤로 옮긴 다음, ftell()을 써서 값을 얻어내거나 fstat()을 쓸 수도 있지만 이 두가지 방법은 같은 단점을 가집니다. 일단 fstat()은 이식성이 뛰어나지 않으며, stat()과 같은 정보를 알려줍니다. ftell()은 바이너리 파일이 아닐 경우 (즉 텍스트 파일), 정확한 바이트 갯수를 알려준다는 보장이 없습니다. 어떤 시스템은 filesize()filelength()와 같은 함수를 제공하지만, 역시 이식성이 뛰어나지 않습니다.

파일을 읽기 전에 그 파일의 크기를 아는 것이 꼭 필요한 지 먼저 생각해 보기 바랍니다. 왜냐하면 C 언어에서 파일 크기를 알아내는 가장 정확한 방법은 파일을 열어서 읽어보는 것이기 때문입니다. 파일을 읽어가며 파일 크기를 계산하는 것도 한가지 해결책이 될 수 있습니다.

References
[C89] § 7.9.9.4
[H&S] § 15.5.1
[PCS] § 12 p. 213
[POSIX] § 5.6.2



Q 19.12b
파일 변경 날짜와 시간을 알아내려면 어떻게 하나요?
Answer
Unix와 POSIX 함수인 stat()을 쓰면 됩니다. 대부분의 다른 시스템에서도 이 함수를 지원합니다. (덧붙여 질문 [*]19.12도 참고하시기 바랍니다.)

Note
TODO: stat()이 표준인지 아닌지 조사!



Q 19.13
파일을 완전히 지우거나 새로 만들지 않고 파일 크기를 줄이는 방법이 없을까요?
Answer
BSD 시스템은 ftruncate() 함수를 제공하고, 몇몇 다른 시스템에서는 chsize() 함수를, 또 어떤 시스템에서는 fcntl() 함수에 (대개 문서화되지 않은) F_FREESP 옵션을 써서 파일 크기를 줄일 수 있습니다. MS-DOS에서는 가끔 write(fd, "", 0)을 써서 해결할 수 있지만 이들은 모두 이식성이 뛰어나지 않습니다. 마찬가지로 파일의 앞 부분을 잘라내는 것도 이식성이 뛰어난 방법은 존재하지 않습니다. 덧붙여 질문 [*]19.14도 참고하시기 바랍니다.



Q 19.14
파일의 중간 쯤에 한 줄(또는 레코드)을 추가하거나 지울 수 있을까요?

Answer
파일을 다시 만들지 않는 한, 거의 불가능하다고 봐야 합니다. (한 레코드를 지우는 경우에는 간단히 지웠다고 표시(marking)하는 것이 간단한 해결책이 될 수 있습니다.) 또, 단순한 일반 파일이 아닌 데이터베이스를 쓰는 것도 한가지 해결책이 될 수 있습니다. 덧붙여 질문 [*]12.30, [*]19.13도 참고하시기 바랍니다.



Q 19.15
주어진 스트림(FILE *)이나, 파일 디스크립터(descriptor)를 써서 파일 이름을 다시 얻어낼 수 있는 방법이 있을까요?
Answer
이 문제는 일반적으로 해결할 수 없습니다. 예를 들어, Unix에서는 (어떤 특별한 권한으로) 디스크 전체를 다시 읽는 방법이 이론상 필요하고, 주어진 디스크립터가 파이프(pipe)나 지워진 파일을 가리키고 있다면 알 수 없으며, 또 파일이 여러 개의 링크를 가지고 있을 경우에는 틀린 이름을 알려 줄 수도 있습니다. 따라서 파일을 열 때, 파일 이름을 따로 저장하는 등의 방법(예를 들어, fopen()의 wrapper 함수를 만들어)을 쓰면 좋습니다.



Q 19.16
파일은 어떻게 지우나요?
Answer
표준 C 라이브러리 함수인 remove()를 쓰면 됩니다. (이 질문은 아마 이 단원에서 “시스템 의존적인 방법을 쓰지 않고” 답할 수 있는 몇되지 않은 질문입니다.) 오래된, ANSI Unix 이전의 시스템에서는 remove()를 제공하지 않을 수도 있습니다. 이 경우, unlink()를 쓰기 바랍니다.

Note
unlink()는 표준 함수가 아닙니다.

References
[K&R2] § B1.1 p. 242
[C89] § 7.9.4.1
[H&S] § 15.15 p. 382
[PCS] § 12 pp. 208, 220-221
[POSIX] § 5.5.1, § 8.2.4



Q 19.16b
파일을 복사하는 방법은?

Answer
system()을 써서 운영 체제에서 제공하는 파일 복사 명령을 쓰거나 (질문 [*]19.27 참고), 원본과 사본 파일을 (fopen()이나 저수준 파일 open 시스템 콜을 써서) 열어 문자 단위 또는 블럭 단위로 데이터를 복사하는 방법을 쓰면 됩니다.

References
K&R § 1, § 7



Q 19.17
왜 절대 경로를 써서 파일을 열 수 없나요? 아래와 같이 호출하면 항상 실패합니다:
  fopen("c:\newdir\file.dat", "r")

Answer
실제 요청한 파일 이름은 -- 문자 \n\f가 쓰였으므로 -- 아마 존재하지 않을 것입니다. 따라서 생각한 것처럼 파일이 열리지 않습니다.

문자 상수나 문자열에서 백슬래시, \는 이스케이프 문자로 해석되어, 뒤따르는 문자에게 특별한 의미를 주는 데에 쓰인다는 것을 기억하기 바랍니다. 백슬래시가 파일 이름19.5에 쓰이기 위해서는 백슬래시를 두번 써서 다음과 같이 만들어야 합니다:

  fopen("c:\\newdir\\file.dat", "r")

MS-DOS에서는 다른 방법을 쓸 수 있습니다. 백슬래시 대신에 슬래시를 써도 디렉토리를 구분하는 문자로 인식되기 때문에 다음과 같이 하면 됩니다:

  fopen("c:/newdir/file.dat", "r")

(그러나, 전처리기 directive인 #include에서 쓰는 파일 이름은 문자열이 아니라는 것을 명심해야 합니다. 따라서 거기에는 백슬래시를 그냥 한번만 써도 됩니다.)

Note
헤더 파일을 포함할 때, 절대 경로를 쓰는 것은 좋지 않은 습관입니다. 왜냐하면, 나중에 헤더 파일의 저장 위치가 바뀔 경우, 소스 파일 전체를 훑어가며 고칠 필요가 생길 지 모르기 때문입니다.

가능한 상대 경로를 쓰기 바라며, 상대 경로가 복잡해질 경우라면, 대부분의 컴파일러는 헤더 파일이 위치한 경로를 지정해 줄 수 있는 옵션을 제공하므로, 그 기능을 쓰시기 바랍니다 (대개 컴파일러는 -I 옵션을 이 목적으로 씁니다.)



Q 19.18
“Too many open files”라는 에러를 봤습니다. 어떻게 하면 동시에 열 수 있는 파일의 갯수를 늘릴 수 있을까요?

두 가지 이유에서 동시에 열 수 있는 파일 갯수에 제한이 있습니다: 하나는 운영 체제에서 쓸 수 있는 저수준 “파일 descriptor”나 “파일 핸들”의 갯수에 제한이 있거나, 표준 입출력(stdio) 라이브러리에서 쓸 수 있는 FILE 구조체의 갯수에 제한이 있기 때문입니다. 이 두 수치가 모두 충분해야 파일을 열 수 있습니다. MS-DOS 시스템에서는 CONFIG.SYS 파일을 수정해서 운영체제가 다룰 수 있는 파일의 갯수를 고칠 수 있습니다. 어떤 컴파일러는 어떤 명령을 (또는 몇몇의 소스 파일을) 써서 stdio FILE 구조체의 갯수를 늘릴 수 있습니다.



Q 19.20
C 프로그램에서 디렉토리를 읽을 수 있습니까?
Answer
POSIX 표준 함수이고, 대부분 Unix 시스템에서 제공하는 opendir()readdir()을 쓸 수 있는 지 조사해 보기 바랍니다. 또 MS-DOS나 VMS, 기타 시스템에서도 이들 함수를 제공하는 경우가 많습니다. (MS-DOS에는 거의 비슷한 일을 해주는 FINDFIRSTFINDNEXT 루틴을 제공하기도 합니다.) readdir()은 단순히 파일 이름만 알려주기 때문에, 파일에 대한 자세한 정보가 필요하다면 stat() 함수를 써야 합니다. 파일이름과 어떤 와일드카드 패턴을 비교하려면 질문 [*]13.7을 참고하기 바랍니다.

Note
디렉토리를 처리하는 방법은, 엄밀히 말해, 표준 C 라이브러리에서는 제공하지 않습니다. 12 절의 머릿말을 참고하시기 바랍니다.
References
[K&R2] § 8.6 pp. 179-184
[PCS] § 13 pp. 230-1
[POSIX] § 5.1
[Dale] § 8



Q 19.22
얼마만큼 메모리가 비어있는지 알 수 있을까요?
Answer
운영체제가 이런 정보를 알려 주는 루틴을 제공할 수도 있지만, 이 방법도 전적으로 시스템 의존적입니다.



Q 19.23
64K보다 큰 배열이나 구조체를 만들 수 있을까요?
Answer
제대로 된 컴파일러라면 사용 가능한 메모리만큼 아무렇게나 써도 동작을 해야 합니다만, 그렇지 못한 컴파일러라면 프로그램에서 쓰는 메모리 양을 줄이던지 아니면 시스템 의존적인 다른 방법을 써야 합니다.

64K는 (현재에도) 상당히 큰 블럭입니다. 여러분의 컴퓨터에 얼마나 많은 메모리가 비어있느냐와는 상관없이, 연속된 64K의 블럭을 할당하는 것은 쉽지 않습니다. (C 표준은 한 오브젝트가 32K보다 클 경우, 아무것도 보장하지 않습니다. [C9X]에서는 64K가 그 제한입니다.) 이 경우, 연속적인 공간이 아니어도 상관없는 그러한 방식으로 쓰는 것이 좋습니다. 예를 들어 동적으로 할당하는 다차원 배열19.6의 경우, 포인터를 가리키는 포인터19.7를 쓸 수 있고, 또 linked list나 포인터의 배열19.8을 쓸 수도 있습니다.

만약 PC 호환 (8086 기반의) 시스템이라면, 그리고 64K 또는 640K 메모리 제한에 걸린다면 “huge” 메모리 모델을 쓰거나 확장 메모리(expanded memory)나 연장 메모리(extended memory)를 쓰는 것도 생각해보기 바랍니다. 또 malloc 계열의 변종인 halloc()이나 farmalloc()을 쓰는 방법도 있고, 32-bit “flat” 컴파일러(예를 들면 djgpp, 질문 [*]18.3 참고)를 쓰는 방법도 있습니다. 또는 DOS extender를 쓰거나 다른 운영체제를 쓰는 것도 생각할 수 있습니다.

References
[C89] § 5.2.4.1
[C9X] § 5.2.4.1



Q 19.24
“DGROUP data allocation exceeds 64K”라는 에러 메시지가 나왔는데 무슨 말인가요? `large' 메모리 모델을 썼으니 64K 이상의 데이터를 쓸 수 있다고 생각하는데요.
Answer
`large' 메모리 모델을 쓰더라도, MS-DOS 컴파일러는 어떤 데이터(문자열이나 전역, 또는 정적 변수들)들을 여전히 default 데이터 세그먼트에 두기 때문에, 이 세그먼트는 여전히 overflow가 일어날 수 있습니다. 전역 변수를 줄이거나, 만약 이미 줄일만큼 줄였다면 (문제가 많은 문자열 때문이라면), 컴파일러에 어떤 옵션을 주어서 default 데이터 세그먼트를 쓰지 않도록 하면 됩니다. 어떤 컴파일러들은 “작은” 데이터들만 default 데이터 세그먼트에 위치시키지만, 이것도 여러분이 선택할 수 있게 해 줍니다 (예를 들어 Microsoft 컴파일러의 경우 /Gt 옵션을 써서 “작은” 데이터가 얼마만큼 작은 것을 뜻하는지 지정할 수 있습니다).



Q 19.25
어떤 주소에 위치한 메모리에 (memory-mapped 장치나 비디오 메모리) 접근하려면 어떻게 하죠?
Answer
적당한 타입의 포인터를 만들어 그 주소를 대입합니다. 이때 캐스트 연산을 써서 컴파일러에게 이식성이 없는 방식을 쓴다는 것을 알려야 합니다:

  unsigned int *magicloc = (unsigned int *)0x12345678;

그러면 `*magicloc'이 여러분이 원하는 위치를 나타내게 됩니다. (MS-DOS에서는 MK_FP()와 같은, 세그먼트와 offset을 나타내는 편리한 방법을 찾을 수 있을 것입니다.)

Note
물론 위의 방법이 100% 동작한다고 보장할 수는 없습니다. 현존하는 대개의 multi-tasking OS는 각각의 프로그램이 다른 프로그램의 메모리에 접근하는 것을 제한하고 있으므로, 응용 프로그램에서 이 방법을 써서 접근하는 것은 거의 불가능합니다. 단, 응용 프로그램이 아닌 OS나 device driver 프로그램은 예외입니다.

References
[K&R1] § A14.4 p. 210
[K&R2] § A6.6 p. 199
[C89] § 6.3.4
[ANSI Rationale] § 3.3.4
[H&S] § 6.2.7 pp. 171-2



Q 19.27
C 프로그램 안에서 다른 프로그램(운영 체제의 명령이나 독자적인 프로그램들)을 부르려면(실행하려면) 어떻게 하죠?

Answer
라이브러리 함수인 system()를 쓰면 됩니다. system()의 리턴 값은 불러서 실행한 프로그램의 끝냄 상태(exit status)라는 것을 기억하시기 바랍니다 (그러나 이는 표준에 정의되어 있지 않기 때문에 꼭 그렇지 않을 수도 있습니다). 이는 대개 그 프로그램의 출력과는 별 상관이 없습니다. 또한 system()이 받는 인자는 하나의 문자열이라는 것을 기억하기 바랍니다. 따라서 여러 개의 (복잡한) 문자열을 한 문자열로 바꿀 필요가 있다면 sprintf()를 쓰면 됩니다. 덧붙여 질문 [*]19.30도 참고하시기 바랍니다.

References
[K&R1] § 7.9 p. 157
[K&R2] § 7.8.4 p. 167, § B6 p. 253
[C89] § 7.10.4.5
[H&S] § 19.2 p. 407
[PCS] § 11 p. 179

Note
[H&S]에 나온 말을 그대로 인용해보면 다음과 같습니다:
The function system passes its string argument to the operating system's command processor for execution in some implementation-defined way. In UNIX systems, the command processor is the shell. The value returned by system is implementation-defined but is usually the completion status of the command.
해석하면 다음과 같습니다:

system()이 전달받는 인자는 운영 체제의 명령 처리기에 전달되어 시스템에 의존적인 방식으로 실행됩니다. UNIX 시스템에서는 명령 처리기는 shell입니다. system()의 리턴 값은 시스템에 의존적이나, 대개는 실행한 명령의 끝냄 코드가 됩니다.



Q 19.30
다른 프로그램이나 명령을 실행시켜서 그 출력을 받아올 수 없을까요?

Answer
Unix와 어떤 시스템들은 popen() 함수를 제공합니다. 이 함수는 stdio 스트림을 지정한 명령을 처리하는 프로세스와 파이프로 연결해 줍니다. 따라서 그 명령의 출력은 (또는 입력도 가능) 여러분의 프로그램에서 읽을 수 있습니다. (덧붙여, pclose() 함수를 불러줘야 합니다.)

popen() 함수를 쓸 수 없다면 system() 함수를 써서 출력이 파일로 저장되게 한 다음, 그 파일을 읽는 방법을 쓸 수도 있습니다.

Unix를 쓰고 있고, popen() 함수로 충분치 않다면, pipe(), dup(), fork(), exec() 함수를 써서 할 수 있습니다.

(한 가지 기억해 두어야 하는 것은 freopen() 함수는 원하는 대로 동작하지 않을 수도 있다는 것입니다.)

References
[PCS] § 11 p. 169



Q 19.31
프로그램 안에서, 프로그램의 절대 경로를 얻어낼 수 있을까요?
Answer
argv[0]이 절대 경로를 나타내거나, 일부분을 나타낼 수도 있지만, 아무것도 나타내지 않을 수도 있습니다. 이 경우 명령 처리기19.9의 검색 경로 알고리즘을 흉내내서 실행 파일의 절대 경로를 얻어낼 수 있습니다. 그러나 이것이 완벽한 해결책이라고 말할 수는 없습니다.

References
[K&R1] § 5.11 p. 111
[K&R2] § 5.10 p. 115
[C89] § 5.1.2.2.1
[H&S] § 20.1 p. 416



Q 19.32
실행 파일이 있는 위치에, 프로그램의 설정 파일을 저장하고 싶은데, 어떻게 하면 그 경로를 찾을 수 있을까요?
Answer
어렵습니다; 질문 [*]19.31의 내용을 먼저 참고하기 바랍니다. 여러분이 그 방법을 알았다고 하더라도, 그 설정 파일의 위치를 바꿀 수 있는 방법(예를 들어 환경 변수를 쓰는 것)을 제공하는 편이 좋습니다. (특히 여러분의 프로그램이 멀티 유져 시스템에서 각각 다른 사람에 의해, 각각 다른 설정 파일을 써서 동작할 필요가 있다면 이 기능은 더욱 중요합니다.)



Q 19.33
프로세스가 그 프로세스를 호출한 parent 프로세스의 environment variable(환경 변수)를 바꿀 수 있나요?
Answer
가능할 수도, 그렇지 않을 수도 있습니다. 대부분의 운영 체제가 환경 변수를 다루는 방식이 Unix와 비슷하기는 하지만 완전히 같지는 않습니다. 환경 변수가 진행 중인 프로그램에 의해 바뀔 수 있는 것이 편리할 수도 있지만, 어쨋든 시스템에 의존적인 문제입니다.

Unix에서 process는 자신의 environment만을 (대부분 setenv(), putenv() 함수를 써서) 고칠 수 있고, 변경된 환경 변수들은 그대로 자식(child) 프로세스에게 전달되지만, 부모(parent) 프로세스에는 전달되지 않습니다. MS-DOS에서는 마스터 환경 변수19.10를 고칠 수 있지만, 이 방법은 매우 복잡합니다. 자세한 것은 MS-DOS FAQ 목록을 참고하기 바랍니다.)



Q 19.36
오브젝트 파일을 읽어서 그 내용을 실행할려면 어떻게 하죠?
Answer
동적 링커(dynamic linker)나 동적 로더(dynamic loader)가 필요합니다. 메모리를 동적으로 할당하고(malloc), 오브젝트 파일을 읽어 오는 것은 가능하지만, 오브젝트 파일 형식의 복잡함을 안다면 여러분은 매우 놀랄지도 모릅니다. BSD Unix에서는 system() 함수와 ld -A 명령을 써서 링크를 할 수 있습니다. 대부분의 SunOS와 System V의 버전들은 -ldl 라이브러리를 써서 오브젝트 파일이 동적으로 로딩되게 할 수 있습니다. VMS에서는 LIB$FIND_IMAGE_SYMBOL을 써서 이 기능을 만들 수 있고, GNU에서는 이 목적으로 “dld” 패키지를 제공합니다. 덧붙여 질문 [*]15.13도 참고하시기 바랍니다.



Q 19.37
주어진 시간만큼 기다리거나, 사용자의 응답 시간을 잴 때, 초 단위보다 더 세밀한 단위를 쓸려면 어떻게 하죠?
Answer
안타깝게도 그런 일을 처리하는 이식성이 뛰어난 방법은 없습니다. V7 Unix와 그 후계 시스템들은 ftime()이라는 매우 쓸모있는 함수를 제공하며, 이 함수는 millisecond 단위로 동작합니다. 또 다른 함수로는 clock(), delay(), gettimeofday(), msleep(), nap(), napms(), nanosleep(), sleep(), setitimer(), times(), usleep()이 있습니다. (Unix에서 제공되는 wait() 함수는 이런 기능이 아닙니다.) select()poll()을 쓸 수 있다면 간단한 기다리기 함수를 만들 수 있습니다. MS-DOS 시스템에서는 시스템 타이머를 프로그래밍하고 타이머 인터럽트를 쓰는 방법을 쓰면 됩니다.

물론, clock() 함수 만이 ANSI 표준입니다. clock() 함수를 두 번 호출하고 그 리턴 값을 비교해서 걸린 시간을 비교할 수 있으며, 그 시간의 단위는 CLOCKS_PER_SEG가 1보다 클 경우, 초 단위보다 더 세밀할 수 있습니다. 그러나 clock()은 현재 프로세스에서만 시간차를 계산할 수 있기 때문에, 멀티태스킹 시스템에서는 진짜 시간과는 약간 다를 수 있습니다.

만약 여러분이 delay 함수를 만들기를 원하고, 시간을 알려주는 함수를 쓸 수 있다면, CPU-intensive busy-waiting을 써서 만들 수 있을 것입니다. 그러나 이는 싱글 태스크 머신에서 혼자 쓸 경우에만 쓸 수 있습니다. 멀티태스킹 운영 체제에서는 여러분의 프로세스가 어떤 기간동안 잠들어 있을 수 있다(예를 들어 sleep(), select(), pause()alarm()setitimer()와 함께 써서)는 것을 명심해야 합니다.

시간을 지연시키는 간단한 방법은 다음과 같이 아무것도 하지 않는 루프를 사용하는 것입니다:

  long int i;
  for(i = 0; i < 1000000; i++)
    ;

그러나, 이러한 코드를 쓰는 것은 될 수 있으면 피하기 바랍니다. 일단 몇 번 반복할 것인가를 잘 선택해야 하며, 더 빠른 프로세서에서는 이 값이 더 늘어날 것입니다. 엎친데 덮친 격으로, 좋은 컴파일러는 위와 같이 아무 것도 하지 않는 코드는 최적화 과정에서 빼 버릴 수 있습니다.

References
[H&S] § 18.1 pp. 398-9
[PCS] § 12 pp. 197-8, pp. 215-6
[POSIX] § 4.5.2



Q 19.38
Control-C와 같은 키보드 인터럽트를 무시하거나 잡아낼 수 있습니까?
Answer
간단한 방법은 다음과 같이 signal() 함수를 쓰는 것입니다:
  #include <signal.h>
  signal(SIGINT, SIG_IGN);

위는 인터럽트 시그널을 무시하는 것이고, 아래처럼 인터럽트 시그널이 발생했을 때 특정 함수인 func()가 불려지게 만들 수도 있습니다:

  extern void func(int);
  signal(SIGINT, func);

Unix와 같은 멀티 태스킹 시스템에서는 다음과 같은 방법을 쓰는 것이 더 좋습니다:

  extern void func(int);
  if (signal(SIGINT, SIG_IGN) != SIG_IGN)
    signal(SIGINT, func);

위에서 조건문과 추가적으로 부른 signal()은 foreground에서 실행된 프로그램에서 발생한 키보드 인터럽트가 background에서 동작하는 프로그램에게 잘못 전달되는 것을 막아 줍니다. (이런 형태의 코드는 signal()을 부르는 방식에 어긋나지 않습니다.)

어떤 시스템에서는 키보드 인터럽트가 터미널 입력 시스템에 따라 영향을 받기도 합니다; 질문 [*]19.1을 참고하기 바랍니다. 어떤 시스템에서는 키보드 인터럽트를 검사하는 것은 프로그램이 입력을 읽고 있을 때에만 적용되며, 그럴 경우, 인터럽트 처리는 어떤 입력 루틴이 쓰이느냐에 따라 달라집니다. MS-DOS 시스템에서는 setcbrk()이나 ctrlbrk() 함수를 쓰기도 합니다.

References
[C89] § 7.7, 7.7.1
[H&S] § 19.6 pp. 411-3
[PCS] § 12 pp. 210-2
[POSIX] § 3.3.1, 3.3.4



Q 19.39
floating-point(실수) exception(예외)를 멋지게 처리하는 방법이 있을까요?
Answer
대부분의 시스템에서는 matherr() 함수를 정의해서 어떤 실수 에러가 (예를 들어 <math.h>안의 함수에 의해) 났을 때, 불려지게 할 수 있습니다. 또는 signal() 함수를 써서 (질문 [*]19.38 참고) SIGFPE를 잡아낼 수도 있습니다. 덧붙여 질문 [*]14.9도 참고하시기 바랍니다.

References
[ANSI Rationale] § 4.5.1



Q 19.40
소켓(socket)을 쓰는 방법은? 또 네트워크 client/server를 만드는 방법은?
Answer
이러한 질문은 이 FAQ 목록의 범위를 벗어납니다. C 언어로 이런 네트워킹 기능을 처리하는 것은 여러 책에서 설명하고 있습니다. 추천할 만한 책으로는 Douglas Comer씨의 3권짜리 책인 Internetworking with TCP/IP와 W. R. Stevens씨의 UNIX Networking Programming이 있습니다. Net에도 “UNIX Socket FAQ”를 포함한 여러 정보가 널려 있습니다. 이 socket FAQ는 다음 URL에서 구할 수 있습니다:
  http://kipper.york.ac.uk/~vic/sock-faq/



Q 19.40b
BIOS를 호출하는 방법은요? TSR을 만드는 방법을 알고 싶습니다.
Answer
이는 어떤 (MS-DOS를 쓰는 PC) 시스템에 매우 의존적인 문제입니다. comp.os.msdos.programmer나 이 그룹의 FAQ 목록을 보면 관련된 정보가 있습니다. 또 Ralf Brown씨의 인터럽트 목록19.11도 참고하기 바랍니다.



Q 19.40c
어떤 프로그램을 컴파일하려 하는데, 컴파일러가 “union REGS”가 정의되어 있지 않다고 에러를 출력하고, 링커는 int86()이 정의되어 있지 않다고 에러를 냅니다.

Answer
모두 MS-DOS 인터럽트 프로그래밍을 하는데에 필요한 것입니다. 다른 시스템에서는 존재하지 않습니다.



Q 19.41
그러나 저는 비표준 함수나 시스템에 의존적인 함수를 쓰면 안되는 처지입니다. 제 프로그램은 ANSI 호환이 되어야 하니까요. 무슨 방법이 없을까요?
Answer
매우 좋지 않은 상황이거나 아니면 처음부터 잘못 이해하고 있는 경우입니다. 왜냐하면 ANSI/[C89] C 표준은 -- 운영체제가 아닌 언어에 대한 정의라서 -- 이러한 기능을 전혀 정의하고 있지 않기 때문입니다. 이러한 분야에 대한 국제 표준은 POSIX19.12에서 다루고 있으며, (Unix 뿐만 아니라) 많은 운영 체제에서 POSIX 호환의 인터페이스를 제공하고 있습니다.

물론 프로그램의 많은 부분을 ANSI 호환으로 작성하는 것은 매우 바람직하며 또 가능합니다. 즉 시스템에 의존적인 부분들만 몇몇의 파일에 따로 모아 두면, 나중에 그 부분만 새로 작성하면 프로그램이 돌아갈 수 있도록 만들면 좋습니다.

Seong-Kook Shin
2018-05-28