프로세스 환경

Soyun Park·2023년 11월 22일
0
post-thumbnail

1. 현재 디렉터리

1-1. getcwd(3)

#include <unistd.h>
char *getcwd(char *buf, size_t bufsize);
  • 프로세스에는 현재 위치의 디렉터리 라는 속성이 있다. 현재 디렉터리를 얻기 위한 함수가 getcwd()이다.
  • getcwd()는 실행 중인 프로세스의 현재 디렉터리를 buf에 넣는다.
  • 성공 시 buf를, 실패 시 NULL을 반환하고 errno를 설정한다.

1-2. path를 위한 버퍼 확보하기

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <limits.h>

int main(int argc, char *argv[]){
    char buf[PATH_MAX];
	
  // getcwd() 함수로 현재 작업 디렉터리의 경로 가져옴
    if (!getcwd(buf, PATH_MAX)){ // 성공 시 0 반환
        perror("getcwd");
        exit(1);
    }
    puts(buf); // buf에 저장된 문자열을 출력
    exit(0);
}
  • getcwd()를 사용하여 buf의 크기를 정할 때 PATH_MAX와 같이 고정적이라면 부족한 경우가 존재할 수 있기 때문에 문제가 된다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#define INIT_BUFSIZE 1024

char* my_getcwd(void){
    char *buf, *tmp;
    size_t size = INIT_BUFSIZE;

    buf = malloc(size);
    if (!buf) return NULL;
    for (;;) {
        errno = 0;
        if (getcwd(buf, size))
            return buf;
        if (errno != ERANGE) break;
        size *= 2;
        tmp = realloc(buf, size);
        if (!tmp) break;
        buf = tmp;
    }
    free(buf);
    return NULL;
}

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

    path = my_getcwd();
    if (!path){
        perror("getcwd");
        exit(1);
    }
    puts(path);
    free(path);
    exit(0);
}
  • 위와 같이 malloc()을 사용해서 버퍼를 확보하고, 버퍼 길이가 부족하면 realloc()으로 버퍼를 증가시킨다.
  • 할당한 메모리는 free()를 통해 해제한다.

1-3. chdir(2)

#include <unistd.h>
int chdir(const char *path);
  • 프로세스의 현재 디렉터리를 변경하려면 chdir을 사용한다.
  • chdir()은 현재 프로세스의 현재 디렉터리를 인자로 지정한 path로 변경한다.

1-4. 다른 프로세스의 현재 디렉터리

  • 다른 프로세스의 현재 디렉터리를 변경하는 API는 없다.
  • 심볼릭 링크 /proc/[PID]/cwd 을 사용하는 방법이 있다.



2. 환경 변수

2-1. 환경 변수란?

  • 프로세스의 부모/자식 관계를 통하여 전파되는 전역 변수와 같다. 항상 설정해 두고 싶은 값을 프로그램에 전달하기 위해 사용한다.
  • 주요 환경 변수는 다음과 같다.


2-2. environ

  • 환경 변수는 전역 변수 environ을 통해 액세스할 수 있다.
  • 형식은 char**이기 때문에 그림으로 표시하면 다음과 같다.

2-3. 현재 프로세스 환경 변수 출력 프로그램 만들기

#include <stdio.h>
#include <stdlib.h>

extern char **environ; // 환경 변수를 가리키는 전역 변수

int main(int argc, char *argv[]){
    char **p;
  
    for (p = environ; *p; p++){
    // environ은 문자열 포인터 배열이고 끝은 널 포인터
        printf("%s\n", *p);
    }
    exit(0);
}
  • environ은 어떤 헤더 파일에도 선언되어 있지 않기 때문에 extern 선언을 한다.
  • environ이 가리키는 주소는 putenv()로 이동할 수 있으므로 변수에 저장해 두고 나중에 접근하면 안된다.

2-4. 작성한 프로그램 실행 예


2-5. getenv(3)

#include <stdlib.h>
char *getenv(const char *name);
  • getenv()는 환경 변수인 name의 값을 검색하여 반환한다.
  • 반환하는 문자열이 putenv() 등으로 인해 이동하는 경우가 있으므로 값을 저장해 두고 반복해서 사용하면 안된다. 또한, 반환값의 문자열에 데이터를 덮어써서도 안된다.

2-6. putenv(3)

#include <stdlib.h>
int putenv(char *string);
  • putenv()는 환경 변수에 값을 설정한다.
  • string은 이름=값 의 형식이어야 한다.
  • putenv()는 전달한 string을 그대로 계속 사용하므로 string의 영역은 정적으로 확보하거나 malloc()으로 할당해야 한다.



3. 자격 증명

3-1. set-uid 프로그램

  • passwd 명령어와 같이 특정 사용자의 권한으로 실행하고 싶은 경우가 있다.

  • 그러나 암호를 변경하기 위해 모든 사용자에게 쓰기 권한을 부여할 수는 없다.

  • 따라서 특정 프로그램에 set-uid 비트를 설정하여 실행한 사용자와 관계없이 프로그램 파일의 소유자 권한으로만 실행되도록 한다.

  • passwd 명령어를 ls -l 하여 소유자의 권한을 살펴보자.

    $ ls -l /usr/bin/passwd
  • 수행 결과는 다음과 같다.

    • rwx 에서 xs 로 바뀌어 있다. 이것이 set-uid 비트가 설정되었다는 표시다.
    • 소유자는 root이기 때문에 passwd 명령어는 root 권한으로만 실행이 가능하다.
    • 이 때, 프로세스를 시작한 사용자 ID를 실제 사용자 ID, set-uid 프로그램 소유자의 ID를 실효 사용자 ID라고 한다.
    • 또한, 이를 지시하는 권한 플래그를 set-gid 비트라고 하고 프로그램을 시작한 사용자 그룹 ID를 실제 그룹 ID, 프로그램의 소유 그룹 ID를 실효 그룹 ID라고 한다.

3-2. 현재 자격 증명 획득

#include <unistd.h>
#include <sys/types.h>

uid_t getuid(void);
uid_t geteuid(void);
gid_t getgid(void);
gid_t getegid(void);
  • 현재 자격 증명을 얻는 시스템 콜은 위와 같다.
  • getuid()는 현재 프로세스의 실제 사용자 ID를 반환한다.
  • geteuid()는 현재 프로세스의 실효 사용자 ID를 반환한다.
  • getgid()는 현재 프로세스의 실제 그룹 ID를 반환한다.
  • getegid()는 현재 프로세스의 실효 그룹 ID를 반환한다.
#include <unistd.h>
#include <sys/types.h>
int getgroups(int bufsize, gid_t *buf);
  • getgroups()는 현재 프로세스의 보조 그룹 ID를 buf에 저장한다.
  • 프로세스의 보조 그룹 ID가 지정한 개수보다 많은 경우 buf에 아무것도 쓰지 않고 오류를 반환한다.

3-3. 다른 자격 증명으로 이행하기

#include <unistd.h>
#include <sys/types.h>

int setuid(uid_t id);
int setgid(gid_t id);
  • setuid()는 현재 프로세스의 실제 사용자 ID와 실효 사용자 ID를 id로 변경한다.
  • setgid()는 현재 프로세스의 실제 그룹 ID와 실효 그룹 ID를 id로 변경한다.
#define _BSD_SOURCE
#include <grp.h>
#include <sys/types.h>
int initgroups(const char *user, gid_t group);
  • initgroups()는 user가 속한 보조 그룹을 현재 프로세스의 보조 그룹으로 설정한다. 이때 두번째 인자로 지정한 group을 추가한다.
  • group은 사용자 그룹을 보조 그룹에도 추가하기 위해 사용한다.
  • initgroups()는 슈퍼 사용자가 아니면 성공할 수 없다.

3-4. 완전히 다른 사용자로 변경하기

  1. 슈퍼 사용자, root로서 프로그램을 시작한다.
  2. 원하는 사용자의 사용자명과 ID, 그룹 ID를 얻는다.
  3. setgid([변경할 그룹 ID])
  4. initgroups([변경할 사용자명], [그룹 ID])
  5. setuid([변경할 사용자 ID])
  • initgroups()는 슈퍼 사용자로 실행해야 하기 때문에 setuid()는 반드시 마지막에 수행한다.



4. 사용자와 그룹

4-1. getpwuid(3), getpwnam(3)

#include <pwd.h>
#include <sys/types.h>

struct passwd *getpwuid(uid_t id);
struct passwd *getpwnam(const char *name);

struct passwd{
	char *pw_name;		/* 사용자 이름 */
    char *pw_passwd;	/* 패스워드 */
    uid_t pw_uid;		/* 사용자 ID */
    gid_t pw_gid;		/* 그룹 ID */
    char *pw_gecos; 	/* 본명 */
    char *pw_dir;		/* 홈 드렉터리 */
    char *pw_shell;		/* 셸 */
};
  • 사용자의 정보를 검색하는 시스템 콜은 위와 같다.
  • getpwuid()는 지정한 id로 사용자 정보를 검색한다.
  • getpwnam()은 지정한 이름으로 사용자 정보를 검색한다.
  • getpwuid()와 getpwnam()은 성공 시 사용자 정보를 struct passwd 타입으로 반환한다.
  • 반환값은 정적으로 할당된 버퍼에 대한 포인터이므로 다시 한번 호출할 경우 덮어쓰일 가능성이 있다.

4-2. getpwnam 명령어 만들기

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>

int main(int argc, char *argv[]){
    // argv: 명령 인자로 사용자 이름을 받음
    struct passwd *pw;

    if (argc < 2){
        fprintf(stderr, "no argument\n");
        exit(1);
    }
  
  	// 사용자 이름이 argv인 사용자 정보를 얻음
    pw = getpwnam(argv[1]); 
  
    if (!pw){
        perror(argv[1]);
        exit(1);
    }
  	
  	// 반환한 사용자 정보 구조체에서 uid를 출력
    printf("id=%d\n", pw->pw_uid);
    exit(0);
}

4-3. 작성한 getpwnam 커맨드 실행 예


4-4. getgrgid(3), getgrnam(3)

#include <grp.h>
#include <sys/types.h>

struct group *getgrgid(gid_t id);
struct group *getgrnam(const char *name);

struct group{
	char *gr_name;		/* 그룹명 */
    char *gr_passwd;	/* 그룹 패스워드 */
    gid_t gr_gid;		/* 그룹 ID */
    char **gr_mem;		/* 그룹에 속하는 멤버(사용자명 리트스) */
};
  • 그룹 정보를 검색하는 시스템 콜은 위와 같다.
  • getgrgid()는 지정한 id로 그룹 정보를 검색한다.
  • getgrnam()은 그룹 이름으로 그룹 정보를 검색한다.
  • getgrgid()와 getgrnam()은 성공 시 struct group 타입으로 반환한다.
  • 반환값은 정적으로 할당된 버퍼의 포인터이기 때문에 다시 한번 호출할 경우 덮어쓰일 가능성이 있다.



5. 프로세스가 사용하는 리소스

5-1. 여러가지 리소스

  • 프로세스가 동작하려면 CPU, 메모리, 디바이스 등의 입출력 등 여러가지 리소스가 필요하다.
  • 커널은 CPU의 사용 시간, 메모리의 사용 크기 등 각 프로세스가 사용하고 있는 리소스의 양을 그때마다 기록한다.
  • 이는 프로그래머가 프로그램을 튜닝할 때나 시스템을 시간당 얼마에 빌려줄지 검토할 경우 등 관점에 따라 다양한 용도로 사용될 수 있다.

5-2. getusage(2)

#include <sys/time.h>
#include <sys/resource.h>
int getrusage(int who, struct rusage *usage);
  • getrusage()는 프로세스의 리소스 사용량을 usage에 저장한다.

  • who가 RUSAGE_SELF 인 경우 현재 프로세스의 리소스 사용량을 기록하고 RUSAGE_CHILDREN 이라면 자식 프로세스의 리소스 사용량을 기록한다.

  • 이 때 자식 프로세스는 현재 프로세스에서 fork()한 모든 자식 프로세스 중에서 wait() 중인 것을 의미한다.

  • struct rusage는 많은 멤버 필드가 있는데 다음과 같다.

    • 해당 프로세스를 위해 커널이 작업한 시간을 시스템 시간이라고 한다.
    • 프로세스가 스스로 소비한 시간을 사용자 시간이라고 한다.
    • 물리 페이지 할당이 일어날 때 저장소와의 입출력이 수반된 것을 메이저 폴트, 수반하지 않은 것을 마이너 폴트라고 한다.
    • 블록 입력과 블록 출력은 HDD나 SSD와 같은 블록 디바이스에 대한 입출력을 말한다.



6. 날짜와 시간

6-1. 유닉스 에폭

  • 리눅스는 시간을 1970년 1월 1일부터 경과한 초 수로 계산한다.
  • 1970년 1월 1일 오전 0시를 유닉스 에폭이라고 한다.
  • 리눅스 커널은 항상 협정 세계시Coordinated Universal Time, UTC로 시간을 계산한다.

6-2. time(2)

#include <time.h>
time_t time(time_t *tptr);
  • time()은 유닉스 에폭부터 현재까지의 경과 초수를 반환한다.
  • time()은 초 단위밖에 취급하지 않으므로 더 정확한 시각이 필요한 경우에는 gettimeofday() 시스템 호출을 사용한다.

6-3. gettimeofday(2)

#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
struct timeval{
	time_t tv_sec;			/* 초 */
    suseconds_t tv_usec;	/* 마이크로초 */
};
  • gettimeofday()는 유닉스 에폭부터 현재까지의 경과 시간을 tv에 저장한다.
  • struct timeval의 tv_sec 멤버에는 초 단위의 값을, tv_usec 멤버에는 마이크로초 단위의 값을 각각 저장한다.

6-4. localtime(3), gmtime(3)

#include <time.h>
struct tm *localtime(const time_t *timep);
struct tm *gmtime(const time_t *timep);
  • localtime()과 gmtime()은 둘 다 time_t 타입으로 표현된 시간을 struct tm 타입으로 변환하여 반환한다.
  • localtim()은 시스템의 로컬 시간대를, gmtime()는 협정 세계시의 시간을 반환한다.
  • struct tm은 연/월/일/시/초를 각각 개별 멤버로 가진다.
  • localtime()과 gmtime()은 정적 버퍼에 반환값 struct tm을 확보하고 있으므로 함수를 다시 호출하면 내용이 덮어쓰인다.

6-5. time 명령어 만들기

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>
#include <locale.h>

int main(int argc, char *argv[]){
    time_t t;
    struct tm *tm;
    struct timeval tv;

    setlocale(LC_TIME, "");

    /* time(2), ctime(3), gettimeofday(2) */
    time(&t);
    gettimeofday(&tv, NULL);
    printf("time         = %ld\n", (long)t);
    printf("ctime        = %s", ctime(&t));
    printf("tv.tv_sec    = %ld\n", (long)tv.tv_sec);
    printf("tv.tv_usec   = %ld\n", (long)tv.tv_usec);
    printf("ctime(tv)    = %s", ctime(&tv.tv_sec));

    /* gmtime(3), localtime(3) */
    tm = gmtime(&t);
    printf("asctime(UTC) = %s", asctime(tm));
    printf("mktime(UTC)  = %ld\n", (long)t);
    tm = localtime(&t);
    printf("asctime(LOC) = %s", asctime(tm));
    printf("mktime(LOC)  = %ld\n", (long)t);

    exit(0);
}

6-6. 작성한 time 커맨드 실행 예


6-7. mktime(3)

#include <time.h>
time_t mktime(struct tm *tm);
  • mktime()은 struct tm 형태로 표현된 tm을 time_t 타입값으로 변환하여 반환한다.

6-8. asctime(3), ctime(3)

#include <time.h>
char *asctime(const struct tm *tm);
char *ctime(const time_t *timep);
  • asctime()은 구조체 struct tm으로 표현된 시간을 Sat Sep 25 00:43:37 2017\n 과 같은 형식의 문자열로 변환하여 반환한다.
  • ctime()은 time_t 타입으로 표현된 시간을 같은 형식의 문자열로 변환하여 반환한다.
  • 모든 반환값은 정적으로 확보한 버퍼의 포인터이므로 다시 호출하면 문자열을 덮어쓰게 된다.

6-9. strftime(3)

#include <time.h>
size_t strftime(char *buf, size_t bufsize, const char *fmt, const stuct tm *tm);
  • strftime()은 tm으로 지정한 시간을 fmt에 따라 포맷하고 buf에 저장한다.

  • fmt는 printf()와 비슷하게 구성하여 출력하고 싶은 시간의 요소를 ['%'+1 문자]로 지정한다.

  • 사용할 수 있는 형식 지정 문자 중 중요한 것은 다음과 같다.

    • c, C, x, X, y, Y는 %와 문자 사이에 E를 놓으면 로케일 의존의 다른 표기를 사용할 수 있다.
    • d, e, H, I, m, M, S, u, U, v, w, W, y는 %와 문자 사이에 O를 놓으면 아라비아 숫자, 로마 숫자, 한자 숫자 등 숫자의 다른 표기를 출력한다.

6-10. strftime 명령어 만들기

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <locale.h>
#include <string.h>

int main(int argc, char *argv[]){
    time_t t;
    struct tm *tm;
    char *fmtchars = "aAbBcCdDeFgGhHIjklmMnOpPrRsStTuUVwWxXyYzZ+";
    char *p;
    char *opt_E = "cCxXyY";
    char *opt_O = "deHImMSuUvwWy";

    setlocale(LC_ALL, "");
    time(&t);
    tm = localtime(&t);
    for (p = fmtchars; *p; p++){
        char fmt[16];
        char buf[256];

        printf("%%%c=", *p);
        fmt[0] = '"';
        fmt[1] = '%';
        fmt[2] = *p;
        fmt[3] = '"';
        fmt[4] = '\0';
        if (strftime(buf, sizeof buf, fmt, tm) == 0)
            puts("FAILED");
        else
            puts(buf);

        if (strchr(opt_E, *p)){
            printf("%%E%c=", *p);
            fmt[0] = '"';
            fmt[1] = '%';
            fmt[2] = 'E';
            fmt[3] = *p;
            fmt[4] = '"';
            fmt[5] = '\0';
            if (strftime(buf, sizeof buf, fmt, tm) == 0)
                puts("FAILED");
            else
                puts(buf);
        }
        if (strchr(opt_O, *p)){
            printf("%%O%c=", *p);
            fmt[0] = '"';
            fmt[1] = '%';
            fmt[2] = 'O';
            fmt[3] = *p;
            fmt[4] = '"';
            fmt[5] = '\0';
            if (strftime(buf, sizeof buf, fmt, tm) == 0)
                puts("FAILED");
            else
                puts(buf);
        }
    }
    exit(0);
}

6-11. 작성한 strftime 커맨드 실행 예


6-12. 시간 포맷에 대한 표준

  • 시간 포맷 중에서 비교적 범용적인 것이 ISO 8601이다.
  • ISO 8601 포맷의 확장 기법에서는 예를 들어 2017년 8월 9일 오후 2시 45분 31초(KST)를 다음과 같이 표시된다.
    2017-08-09T14:45:31+09:00
  • 이러한 표기법은 다음과 같은 특징을 가진다.
    • 연도는 항상 4자리
    • 월, 일, 시, 분, 초는 항상 0을 채움으로 2자리
    • 연월일의 구분 문자는 /가 아닌 -
    • 날짜와 시간 사이는 T가 들어감
    • 시는 항상 24시간제
    • UTC와의 시차를 +09:00 과 같은 형식으로 추가
  • strftime()으로 ISO 8601 포맷으로 시간을 출력할 때는 포맷 문자열 '%FT%T%z'를 사용하여 포맷한 후 마지막 두 문자 앞에 : 을 추가하면 된다.

6-13. 시간 관련 API 정리

  • 시간 관련 API의 데이터 구조와 관계는 다음과 같다.



7. 로그인

7-1. 로그인 중에 일어나는 과정

  1. systemd 또는 init이 단말 수만큼 getty 명령어를 기동
  2. getty 명령어는 단말로부터 사용자 이름을 입력하는 것을 기다려, login 명령어를 시작
  3. login 명령어가 사용자를 인증
  4. 셸을 시작

7-2. systemd와 getty

  • systemd는 커널이 직접 시작하는 유일한 프로그램으로 로그인이라는 getty 프로그램을 시작하는 역할을 한다.
  • getty는 단말을 open()하고 read()해서 사용자가 사용자명을 입력하는 것을 기다린다.
  • 사용자명이 입력되면 getty는 dup()를 사용하여 파일 디스크립터 0번, 1번, 2번에 단말을 연결하고 새로운 프로그램 login을 exec한다.

7-3. 인증

  • login은 사용자를 인증해야 한다.
  • 사용자 데이터베이스의 위치는 설정에 따라 달라지며, 가장 일반적인 위치는 /etc/passwd 이다.
  • 여러 시스템 간에 데이터베이스를 공유하기 위해 NIS 또는 LDAP과 같은 디렉터리 서비스를 사용할 수 있다.
  • 암호는 /etc/passwd 에 직접 쓰지 않고 /etc/shadow 로 분리하는 섀도 패스워드가 도입되어 있다.
  • 또한 다음과 같은 사항을 시스템별로 커스터마이즈할 수 있도록 해야 한다.
    • 패스워드 종류, 문자 수 제한, 암호화 방법
    • 로그인할 수 있는 날짜나 시간의 제한
    • 로그인 할 수 있는 사용자 제한
  • login 명령어는 이러한 항목을 모두 /etc/login.defs 에서 설정하게 되어있다.

7-4. 로그인 셸

  • 인증 후 다음과 같이 셸을 exec 한다.
    execl("/bin/sh", "-sh", ...);
  • 이처럼 시작한 셸을 로그인 셸이라고 한다.

7-5. 로그인 기록

  • login이나 systemd는 누가 어떤 단말로 로그인했는지 기록을 파일로 관리한다.
  • w 명령어는 현재 로그인된 사용자를 출력하는 명령어이고 해당 정보는 리눅스의 /var/run/utmp에 있다. 명령어 수행 결과는 다음과 같다.

  • last 명령어가 출력하는 과거 로그인 정보는 /var/log/wtmp 에 있다.
  • 그 외 lastlog 명령어가 보는 /var/log/lastlog 도 있다.

0개의 댓글