[Linux] 리눅스 시스템 프로그래밍 개요

시스템 프로그래밍

시스템 호출(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;
}

 

포인터와 동적 메모리 할당

정수형 변수와 포인터 변수

  1. 정수형 변수(Integer Variable) : 데이터(값) 그 자체를 저장하는 변수
    • 할당 크기: 자료형(short, int, long)에 따라 다르지만, int는 대개 4바이트(32비트)로 고정됨
    • 특징: 시스템이 64비트로 바뀌어도 int의 크기는 보통 그대로 4바이트를 유지 (호환성 때문)
    • 코드화 방법: 자료형 뒤에 변수명을 쓴다. (e. int a = 10;)
  2. 인터 변수(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)