시스템 프로그래밍
시스템 호출(system call)
: 커널 서비스(함수) 요청을 위한 인터페이스. 응용 프로그램은 시스템 호출을 통해 커널에 서비스를 요청
- 운영체제의 커널이 제공하는 다양한 서비스를 이용해 프로그램을 구현할 수 있도록 제공되는 프로그래밍 인터페이스 (운영체제마다 다름)
- C언어의 함수를 호출하는 형식으로 이용

라이브러리(library)
: 유용한 함수들을 컴파일한 오브젝트 파일들의 묶음. 리눅스에서는 보통 /usr/lib에 저장됨 (libXXX.a 혹은 libXXX.so.VER)
- 라이브러리 함수(library function) : 라이브러리에 포함된 함수
- 표준 라이브러리 함수: 표준화된 인터페이스 제공 (리눅스의 표준 C 라이브러리:
libc.so.6)
시스템 호출 vs. 표준 라이브러리 함수

응용 프로그램의 빌드

printf()의 출력 동작
: printf() 함수는 표준 라이브러리 함수이다.
: 디스플레이에 접근하는 것은 커널만 가능하므로, printf() 함수는 디스플레이에 직접 출력할 수 없다. 따라서 printf()는 C 표준 라이브러리의 버퍼에 출력된다.
: if. 버퍼가 꽉 차면 → printf()는 시스템 호출 함수 write() 호출 → write() 함수는 '시스템 호출 CPU 명령' 실행 → 커널에 작성된 함수가 디스플레이에 "hello" 출력

탐구. write 시스템 호출
: 리눅스에서 어셈블리어를 이용하여 직접 syscall 기계 명령으로 시스템 호출을 일으키는 사례이다.

fread()와 read()의 동작 과정

man 페이지의 섹션 번호
: 매뉴얼은 항목 종류에 따라 섹션이 구분
- 섹션1: 일반적인 명령에 대한 설명
- 섹션2: 시스템 호출
- 섹션3: 표준 라이브러리 함수
: man -s <섹션번호> <명령어/함수이름>
- -s 옵션 미지정시 섹션 번호가 낮은 항목이 기본 출력됨 (e. man -s 2 open, man -s 3 fopen)
시스템 호출의 오류 처리
- 처리 성공 시: 0 or 양의 정수 반환
- 처리 실패 시: -1 반환. 전역변수 errno에 오류 코드 저장 (유닉스: /usr/include/sys/errno.h, 리눅스: /usr/include/asm-generic/errno-base.h)
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
extern int errno;
int main(void)
{
if (access("test.txt", F_OK) == -1) {
printf("errno=%d\n", errno);
}
return 0;
}
오류 메시지 출력
: errno에 저장된 값을 읽어 그에 해당하는 메시지를 표준 오류 출력에 출력
#include <stdio.h>
void perror(const char* s);
- 인수로 보통 프로그램 이름을 지정
#include <stdio.h>
#include <unistd.h>
int main(void)
{
if (access("test.txt", F_OK) == -1) {
//printf("errno=%d\n", errno);
perror("test.txt");
}
return 0;
}
포인터와 동적 메모리 할당
정수형 변수와 포인터 변수
- 정수형 변수(Integer Variable) : 데이터(값) 그 자체를 저장하는 변수
- 할당 크기: 자료형(short, int, long)에 따라 다르지만, int는 대개 4바이트(32비트)로 고정됨
- 특징: 시스템이 64비트로 바뀌어도 int의 크기는 보통 그대로 4바이트를 유지 (호환성 때문)
- 코드화 방법: 자료형 뒤에 변수명을 쓴다. (e. int a = 10;)
- 포인터 변수(Pointer Variable) : 데이터가 저장된 메모리 주소(Address)를 저장하는 변수
- 할당 크기: 가리키는 데이터의 타입과 상관없이 크기 일정. 오직 시스템의 주소 체계(비트 수)에 따라 결정 (e. 32비트 시스템: 모든 포인터는 4바이트 / 64비트 시스템: 모든 포인터는 8바이트)
- 특징: char*(1바이트를 가리키는 포인터)나 double*(8바이트를 가리키는 포인터)나 변수 자체의 크기는 똑같다.
- 코드화 방법: 자료형 뒤에 *를 붙인다. (e. int *1;)

포인터 사용의 이점
- 함수의 매개변수로 사용: 프로그램의 다른 부분에 있는 데이터를 공유할 수 있게 해주고, 큰 데이터 구조를 간단한 방법으로 참조할 수 있게 한다.
- 동적할당메모리를 가리킴 : 프로그램이 실행되는 동안 새로운 메모리를 확보할 수 있게 한다.
메모리 할당
- 정적할당(static allocation): 전역변수, 프로그램이 종료할 때까지 유지. 메모리의 고정된 위치에 할당
- 자동할당(automatic allocation): 지역변수, stack에 할당. 함수가 호출되면 할당, 종료되면 반납
- 동적할당(dynamic allocation): 프로그래머가 지정한 곳에서 heap에 할당, 프로그래머가 지정한 곳에서 반납

동적할당을 쓰는 이유?
보통 변수를 선언할 때는int arr[10];처럼 크기를 미리 지정한다(정적 할당). 하지만 프로그램을 짜다보면 데이터가 몇 개나 들어올지 미리 알 수 없는 경우가 많다.
- 정적할당(배열): "학생이 30명이겠지?" 하고arr[30]을 만들었는데, 31명이 오면 프로그램이 터진다.
- 동적 할당(malloc): "학생이 들어오는 대로 의자를 하나씩 더 놓자." (유연함)
동적 메모리 할당
: 실행 시 필요한 크기의 메모리를 할당 받아 명시적으로 반납할 때까지 사용
: void *malloc(size_t nBytes);
- 동적 메모리 할당을 수행하는 가장 대표적인 함수 (from Memory Allocation) "
stdlib.h를 통해 제공- 반환값: 동적 할당된 메모리의 시작주소를 반환. 할당 실패시
NULL포인터 상수 반환
int* ip;
ip = (int*) malloc(sizeof(int));

동적 배열
: 동적 할당을 사용하는 일반적인 경우
- heap에 배열을 할당하고, 그 값을 조작하기 위해 포인터 변수를 이용
int *ip;
ip= (int*) malloc(sizeof(int) * 2);

동적 할당된 메모리의 반납
: 메모리가 고갈되지 않도록 하는 방법. 더 이상 필요 없는 동적 할당된 메모리의 반납
- 반드시 동적 할당된 메모리는 더 이상 필요 없는 시점에 반납하는 습관을 갖자!
void free(void*);
// 동적으로 할당받은 메모리의 주소값을 갖고 있는 포인터 변수를 인수로 취함
명령행 인자
명령행 인자(CLA)
: main() 함수도 호출 시 인수를 전달받음
: int main(int argc, char* argv[]);
argc: 인수의 개수argv: 문자열 형태로 전달되는 인수 배열 /argv[0]: 실행파일명
#include <stdio.h>
int main(int argc, char* argv[])
{
printf("argc = %d\n", argc);
for (int n = 0; n < argc; n++)
printf("argv[%d] = %s\n", n, argv[n]);
return 0;
}
옵션 처리
: 명령행 인자로 전달된 옵션을 편리하게 처리하도록 getopt() 함수 제공
#include <unistd.h>
int getopt(int argc, char* const argv[], const char* optstring);
extern char* optarg;
extern int optind, opterr, optopt;
optstring: 옵션을 나타내는 문자에 대한 문자열 지정. 어떤 옵션을 허용할지 정의하는 문자열 (옵션 뒤에 필수 인자가 있는 경우:을 덧붙임)optarg: Option Argument. 옵션 뒤에 따라오는 실제 값을 가리키는 포인터optopt: Option Option. 알 수 없는 옵션이 들어왔을 때, 그 문자를 저장optind: 다음에 처리할 argv의 인덱스가 저장됨- 반환값: 옵션 문자를 반환, 오류시 ? 반환 (옵션 뒤 필수 인자는 optarg에 저장됨)
옵션 처리의 예
#include <stdio.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
int n;
extern char* optarg;
extern int optind;
printf("Currenr Optind : %d\n", optind);
while ((n = getopt(argc, argv, "abc:") != -1) {
swich (n) {
case 'a':
printf("Option : a\n");
break;
case 'b':
printf("Option : b\n");
break;
case 'c':
printf("Option : c, Argument=%s\n", optarg);
break;
}
printf("Next Optind : %d\n", optind);
}
return 0:
}
구조체
구조체 사용 예
struct point {
int x;
int y;
};
int main(void)
{
struct point p1, p2;
...
return 0;
}

구조체 타입의 사용 예 ①
struct point {
int x;
int y;
};
typedef struct point pointT;
int main(void)
{
pointT p1, p2;
...
return 0;
}

구조체 타입의 사용 예 ②
typedef struct point {
int x;
int y;
} pointT;
int main(void)
{
pointT p1, p2;
...
return 0;
}

구조체와 인수
- 구조체를 인수로 넘길 때: 일반 변수의 경우와 마찬가지로 실인수의 복사본이 매개변수로 전달됨
- 구조체를 함수의 변환값으로 넘길 때: 일반 변수의 경우와 마찬가지로 반환값의 복사본이 호출측의 임시변수로 전달됨
⇒ 큰 구조의 구조체 변수라면 시간과 메모리가 소요됨 (일반적으로 구조체에 대한 포인터를 남김)
구조체 포인터
: 구조체를 가리키는 포인터. 포인터 자체만을 위한 메모리를 확보하며, 사용시에는 적절한 초기화 필요

구조체 포인터로 필드 접근

- (*ps).number = 24; // 기본적인 접근
- ps -> number = 24; // 간편한 접근 위해
- *ps.number = 24; // err: *(ps.number)
- *ps -> number = 24; //err: *(ps -> number)