메시지큐, 공유메모리 등 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

메시지 큐

IPC기법 중 하나인 메시지큐는 Data Structure 중 큐를 사용합니다. 기본적으로는 먼저온 메시지가 먼저 꺼내어집니다. 메시지큐의 msgtype에 따라 특정 메시지 중 가장 먼저들어온 메시지도 받아올 수 있습니다. 이 메시지는 커널에서 보관하고 있으니 프로세스가 종료되어도 사라지지 않습니다.

메시지 큐의 용량이 허용하는 한 메시지는 계속 큐에 쌓일 수 있습니다. 메시지를 얻어오는 쪽은 가장 메시지를 읽고 메시지큐에서 그 메시지를 삭제합니다.

 

메시지 큐에는 두가지의 종류가 있습니다.

- System V의 Message Queue

- POSIX의 Message Queue

mq_open, mq_send, mq_receive와 같은 함수를 사용하는 POSIX에서 메시지큐 활용방법을 알아보시려면 아래의 포스팅을 참고하시면 됩니다.

https://reakwon.tistory.com/209

 

[리눅스] POSIX 메시지 큐 사용 방법 - 예제 코드

Message Queue 프로세스 간 통신 방식인 IPC기법 중 하나인 메시지 큐는 두 가지 사용 방법이 있습니다. msgget, msgsend, msgrecv와 같은 함수를 사용하는 방식인 System V 방식의 메시지큐, 그리고 지금 알아

reakwon.tistory.com

이번 포스팅에서는 System V의 Message Queue를 리눅스에서 어떻게 이용할 수 있는지 알아보도록 합시다.

 

메시지 큐 관련 시스템 콜

메시지큐를 사용하기 위해서는 3개의 헤더파일이 필요합니다.

 #include <sys/msg.h>
 #include <sys/ipc.h> 
 #include <sys/types.h>

 

1) msgget : System V의 메시지 큐 id를 얻어옵니다.

 int msgget(key_t key, int msgflg);

 

key : 메시지큐를 얻어올 때 사용하는 고유 key값입니다.

msgflg : flag에는 2가지가 있는데요. IPC_CREAT과 IPC_EXCL입니다.

 - IPC_CREAT : 메시지큐가 없으면 새로 생성합니다.

 - IPC_EXCL : IPC_CREAT과 같이 사용하는 flag인데, 만약 해당 메시지큐가 존재하면 msgget은 오류를 반환합니다.

 

2) msgsnd : 메시지를 보냅니다. 

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

 

msqid : 메시지 큐의 id입니다.

msgp : msgp는 void*이나 우리는 구조체 형식으로 아래와 같이 정의해주어야합니다. 

struct msgbuf {
       long mtype;       /* message type, must be > 0 */
       char mtext[1];    /* message data */
};

 

- mtype : 메시지의 타입을 나타냅니다. 이 값은 0보다 커야한다고 주석이 말해주고 있군요. 메시지큐의 msgbuf를 정의할때 long형의 mtype을 반드시 명시해주어야합니다. 

- mtext : mtext은 실제 메시지 큐에 보낼 데이터를 말합니다. mtext와 같이 배열일 수도 있고, 구조체일 수도 있습니다. 

msgsz : msgsz는 메시지 큐에 전송할 데이터의 사이즈를 의미하는데, 위의 msgbuf의 mtype멤버를 제외한 실데이터의 크기를 전달해야합니다. 

msgflg : 큐의 공간이 없을때 msgsnd의 동작은 blocking입니다. 즉, msgsnd에서 큐의 공간이 생겨날때까지 기다리는 것이지요. 여기서 IPC_NOWAIT을 사용한다면 msgsnd는 blocking되지 않고 실패합니다.

msgsnd가 성공적으로 동작하면 msqid_ds라는 구조체의 필드의 값이 변경됩니다. sys/msg.h에 이 구조체가 정의되어있습니다.

 

struct msqid_ds {
       struct ipc_perm msg_perm;     /* Ownership and permissions */
       time_t          msg_stime;    /* Time of last msgsnd(2) */
       time_t          msg_rtime;    /* Time of last msgrcv(2) */
       time_t          msg_ctime;    /* Time of last change */
       unsigned long   __msg_cbytes; /* Current number of bytes in
                                                queue (nonstandard) */
       msgqnum_t       msg_qnum;     /* Current number of messages
                                                in queue */
       msglen_t        msg_qbytes;   /* Maximum number of bytes
                                                allowed in queue */
       pid_t           msg_lspid;    /* PID of last msgsnd(2) */
       pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
};

이 구조체는 메시지큐의 메타데이터같은 것인데 msgsnd를 성공적으로 호출하고 나면 아래와 같이 변경됩니다.

- msg_lspid는 호출된 process id로 변경

- msg_qnum의 값 1 증가

- msg_stime을 현재 시간으로 설정

 

3) msgrcv : msgsnd를 했다면 받는 시스템 콜이 있어야겠지요. msgrcv가 그 역할을 합니다. 메시지큐 id의 메시지를 하나 읽고 그 메시지를 큐에서 제거합니다.

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

 

msqid : 다 아시겠지만 메시지큐 id입니다.

msgp : 읽어들인 메시지를 msgp가 가리키는 주소에 위치시킵니다. 쉽게말해 읽어온 메시지입니다.

msgsz : 메시지를 읽을 크기(더 정확히는 msgbuf의 text 크기입니다.)인데 만약 읽어들인 메시지가 지정된 크기보다 크다면 msgflg에 따라 동작이 결정됩니다. 만약 msgflg가 MSG_NOERROR라면 메시지를 읽어들이나 잘려서 읽히게 됩니다. MSG_NOERROR가 명시되어있지 않다면 메시지를 읽어오지 않고 msgrcv 시스템콜은 -1을 반환합니다.

msgtyp : 0, >0, <0으로 동작이 나뉩니다.

msgtyp == 0 큐의 첫번째 메시지를 읽어옵니다.
msgtyp > 0 그 값과 동일한 메시지 타입을 갖는 메시지를 반환합니다. 
msgtyp < 0 msgtyp의 절대값 이하의 가장 작은 메시지를 읽어옵니다.

msgflg : 4개 정도의 flag가 있습니다. 

- IPC_NOWAIT

- MSG_COPY : 리눅스 3.8이상부터 지원합니다.

- MSG_EXCEPT

- MSG_NOERROR

 

4) msgctl : 메시지큐를 제어하기 위한 시스템 콜입니다. 

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

msqid : 다들 아시죠? 메시지 큐 id입니다.

cmd : 제어할 command입니다. 몇개 있는데 3개만 보도록 하겠습니다.

- IPC_STAT : buf로 메시지큐 정보를 읽어옵니다. 

- IPC_SET : buf로 메시지큐 정보를 설정합니다.

- IPC_RMID : 메시지큐를 지웁니다.

buf : 아까 위에서 보았던 구조체네요. cmd로 IPC_STAT과 IPC_SET을 사용할때 전달해주면 되고 

딱히 필요없으면 NULL을 전달합니다.

 

이제 메시지큐를 통한 예제를 볼까요?

아래의 예제는 sender에서 어떤 사람의 나이와 이름을 receiver에게 전달하는 메시지 큐 예제입니다. sender에서는 메시지큐의 정보 또한 보여주고 있습니다. 메시지 큐에 있는 message라는 구조체는 sender와 receiver가 사용하고 있으므로 헤더파일 msg_data.h를 두어 같이 사용합니다.

 

예제는 너무 간단하기 때문에 따로 설명할 필요는 없고 한번 따라 해보면 다 이해하실거에요.

 

msg_data.h

struct real_data{
        short age;
        char name[16];
};
struct message{
        long msg_type;
        struct real_data data;
};

 

sender.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#include "msg_data.h"


void printMsgInfo(int msqid){

        struct msqid_ds m_stat;
        printf("========== messege queue info =============\n");
        if(msgctl(msqid,IPC_STAT,&m_stat)==-1){
                printf("msgctl failed");
                exit(0);
        }
        printf(" message queue info \n");
        printf(" msg_lspid : %d\n",m_stat.msg_lspid);
        printf(" msg_qnum : %d\n",m_stat.msg_qnum);
        printf(" msg_stime : %d\n",m_stat.msg_stime);

        printf("========== messege queue info end =============\n");
}
int main(){
        key_t key=12345;
        int msqid;

        struct message msg;
        msg.msg_type=1;
        msg.data.age=80;
        strcpy(msg.data.name,"REAKWON");

        //msqid를 얻어옴.
        if((msqid=msgget(key,IPC_CREAT|0666))==-1){
                printf("msgget failed\n");
                exit(0);
        }

        //메시지 보내기 전 msqid_ds를 한번 보자.
        printMsgInfo(msqid);

        //메시지를 보낸다.
        if(msgsnd(msqid,&msg,sizeof(struct real_data),0)==-1){
                printf("msgsnd failed\n");
                exit(0);
        }

        printf("message sent\n");
        //메시지 보낸 후  msqid_ds를 한번 보자.
        printMsgInfo(msqid);
}

 

receiver.c 

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#include "msg_data.h"

int main(){
        key_t key=12345;
        int msqid;
        struct message msg;

        //받아오는 쪽의 msqid얻어오고
        if((msqid=msgget(key,IPC_CREAT|0666))==-1){
                printf("msgget failed\n");
                exit(0);
        }
        //메시지를 받는다.
        if(msgrcv(msqid,&msg,sizeof(struct real_data),0,0)==-1){
                printf("msgrcv failed\n");
                exit(0);
        }

        printf("name : %s, age :%d\n",msg.data.name,msg.data.age);

        //이후 메시지 큐를 지운다.
        if(msgctl(msqid,IPC_RMID,NULL)==-1){
                printf("msgctl failed\n");
                exit(0);
        }
}

 

두개의 c파일을 컴파일 해줍시다.

# gcc sender.c -o sender
# gcc receiver.c -o receiver

 

이제 sender부터 실행하도록 합시다.

# ./sender
========== messege queue info =============
 message queue info
 msg_lspid : 0
 msg_qnum : 0
 msg_stime : 0
========== messege queue info end =============
message sent
========== messege queue info =============
 message queue info
 msg_lspid : 5129
 msg_qnum : 1
 msg_stime : 1586521322
========== messege queue info end =============

 

메시지 큐의 정보가 변경된 것을 알 수있네요. 메시지가 성공적으로 보내졌나봅니다.

ipcs를 구경해볼까요?

# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x00003039 5          root       666        18

 

0x3039는 우리가 정한 key 12345입니다. sender 프로그램이 끝나고 메시지 큐는 아직 남아있습니다. 

이제 receiver를 실행해보도록 하지요.

# ./receiver
name : REAKWON, age :80

 

음, 잘 읽히네요. 한번더 receiver를 실행하면 아무런 동작을 하지 않습니다. 메시지큐에 아무것도 없기 때문입니다. ipcs를 들여다보면 메시지큐가 삭제되었네요. 그 이유는 receiver 코드를 보면 msgctl에서 cmd를 IPC_RMID를 주어 제거했기 때문입니다. 

# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

 

이상으로 메시지큐와 관련한 포스팅을 마치도록 하겠습니다. 긴글 읽어주셔서 감사합니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

공유 메모리, 메시지큐 등 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

공유메모리(Shared Memory)

프로세스에서 메모리는 해당 프로세스만이 사용하는게 일반적입니다. 메모리에는 명령어, 지역 변수, 동적 변수, 전역 변수와 같이 데이터가 존재하는데 그 프로세스만 접근할 수 있고 변경가능합니다. 일반적으로 이렇다는 건데, 아주 가끔 이 데이터가 다른 프로세스에서 쓰일 수 있도록 만들 수도 있습니다. 그게 바로 공유메모리라는 IPC기법입니다.

서로 다른 프로세스가 특정 메모리를 공유하면 데이터를 더 빠르게 접근할 수 있기 때문에 프로그램을 더 효율적으로 만들 수 있습니다. 이해도 직관적으로 할 수 있기 때문에 코드도 그렇게 어렵지가 않습니다.

 

관련 함수

공유메모리를 사용하기 위해서 우리가 필요한 헤더파일은 아래와 같이 2개입니다.

 #include <sys/shm.h> 
 #include <sys/ipc.h>

 

1) shmget : 인자로 전달된 key의 값으로 공유메모리를 얻고 공유메모리 조각(shared memory segment)의 id를 돌려줍니다. 

int shmget(key_t key, size_t size, int shmflg);

key : 공유메모리를 할당할때 사용하는 고유 key값입니다.

size : 메모리의 최소 size를 의미합니다. 새로운 공유메모리를 할당받는다면 size를 명시하고 이미 존재하는 메모리면 0을 주면 됩니다.

shmflg : IPC_CREATIPC_EXCL 두 가지가 존재합니다.

- IPC_CREAT : 새로운 메모리 세그먼트는 만듭니다. 이 flag를 사용하지 않는다면 shmget은 명시된 key와 연관된 찾고 접근할 수 있는 권한이 있는지 확인합니다.

 

- IPC_EXCL : IPC_CREAT과 함께쓰는 플래그로 만약 메모리 세그먼트가 존재하면 shmget은 실패하게 됩니다.

IPC_EXCL을 사용하는 경우는 우선 공유메모리가 있는지 확인 후 없으면 IPC_CREAT을 통해 할당받으라는 뜻입니다. 이렇게 공유메모리가 오염되는 것을 방지할 수 있습니다.

 

2) shmat : 공유메모리를 얻었으면 메모리의 위치에 이 프로세스를 묶는(attach) 시스템 콜입니다.

void *shmat(int shmid, const void *shmaddr, int shmflg);

shmid : 공유메모리의 id를 의미합니다. shmget을 통해 얻어올 수 있습니다.

shmaddr

-NULL(0)일 경우 : 커널에서 적절한 주소를 반환하여 줍니다.

-NULL이 아닐 경우 : shmflg로 SHM_RND일때, 그 주소와 attach할 가장 가까운 주소를 반환합니다.

 

정상적으로 동작한다면 적절한 포인터를 넘기고 실패하면 (void*) -1을 반환하게 됩니다. 여기서 왜 void*를 반환하는지 아시겠죠? 공유 메모리에 들어가있는 데이터가 정수형인지, 부동 소수 형태인지, 또는 구조체인지 모르니까 무엇이든 받을 수 있는 void*으로 넘겨줄테니 알아서 반환해서 써라 이 이야기입니다.

 

3) shmdt : 공유메모리를 이 프로세스와 떼어냅니다. 이는 공유메모리를 제거하는 것이 아님에 주의하세요.

int shmdt(const void *shmaddr);

shmaddr : shmat에서 전달받은 그 포인터를 전달하면 됩니다.

성공 시 0, 실패시 -1을 반환합니다.

 

4) shmctl : 공유메모리를 제어하기 위해 사용합니다. 예를 들면 공유메모리의 정보를 얻거나 어떤 값을 쓰거나 공유메모리를 삭제하는 등의 조작이 있습니다.

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmid : 말 안해도 아시겠지만 공유메모리 id입니다.

cmd : 제어할 일종의 command입니다. 정수형을 갖으며 여러 command가 존재하는데 우리는 IPC_RMID라는 cmd를 예제를 통해 사용하도록 하겠습니다.

buf : shmid_ds라는 구조체로 정의되어 있네요. 어떤 구조체인지 봅시다.

struct shmid_ds {
       struct  ipc_perm shm_perm;    /* Ownership and permissions */
       size_t          shm_segsz;   /* Size of segment (bytes) */
       time_t          shm_atime;   /* Last attach time */
       time_t          shm_dtime;   /* Last detach time */
       time_t          shm_ctime;   /* Last change time */
       pid_t           shm_cpid;    /* PID of creator */
       pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
       shmatt_t        shm_nattch;  /* No. of current attaches */
       ...
};

 

복잡하게 보이지만 사실 옆의 주석만 좀 읽어보면 추측은 할 수 있겠네요. 

shm_perm : 공유메모리의 접근제어가 저장되어있습니다.

shm_segsz : 공유메모리의 size를 의미합니다.

shm_atime : attach된 시간 정보를 기록하네요.

shm_dtime : 반대로 detach된 시간을 기록합니다.

shm_ctime : 공유메모리가 변경될때의 시간을 기록합니다.

shm_cpid : 이 공유메모리를 최초로 만들어낸 process id입니다.

shm_lpid : 최근 shmat, 또는 shmdt를 수행한 process id입니다. 

 

첫번째 필드인 ipc_perm이라는 구조체도 한번 볼까요?

struct ipc_perm {
       key_t          __key;    /* Key supplied to shmget(2) */
       uid_t          uid;      /* Effective UID of owner */
       gid_t          gid;      /* Effective GID of owner */
       uid_t          cuid;     /* Effective UID of creator */
       gid_t          cgid;     /* Effective GID of creator */
       unsigned short mode;     /* Permissions + SHM_DEST and
                                           SHM_LOCKED flags */
       unsigned short __seq;    /* Sequence number */
};

 

아실테지만 uid, gid 등의 permission 정보가 있습니다. 

 

 

 

 

예제 

이제 공유메모리를 사용한 아주 간단한 예를 보도록 하겠습니다. 너무나 간단합니다.

#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(){
        int shmid;
        int *num;
        key_t key=987654;
        void *memory_segment=NULL;

        if((shmid=shmget(key,sizeof(int),IPC_CREAT|0666))==-1){
                printf("shmget failed\n");
                exit(0);
        }


        if((memory_segment=shmat(shmid,NULL,0))==(void*)-1){
                printf("shmat failed\n");
                exit(0);
        }

        num=(int*)memory_segment;
        (*num)++;
        printf("shared memory value :%d\n",(*num));
        return 0;
}

 

공유메모리 id를 얻어와서 공유메모리의 값을 하나씩 증가시키고 있는 예제입니다. 보시다시피 공유메모리의 포인터는 void*를 반환하지만 우리는 int*로 연산할 것이기에 int*형변환을 하고 값을 증가시켰지요.

사용되는 key값은 아무 정수나 넣어주시면 됩니다. 

 

자, 컴파일하고 a.out을 연타해봅시다.

# gcc shm.c
# ./a.out
shared memory value :1
# ./a.out
shared memory value :2
# ./a.out
shared memory value :3
# ./a.out
shared memory value :4
# ./a.out

 

본래 프로세스가 끝나면 메모리도 정리가 되서 계속 1이라는 값이 출력이 되어야하는데 메모리가 아직 살아있고 그 메모리의 값을 참조하여 증가시키니 프로그램을 계속 실행시키면 이렇게 증가된 값을 보여주게 됩니다.

공유메모리가 아직 살아있다는 것을 어떻게 알까요?

 

ipcs -m 명령으로 볼 수 있습니다.

# ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 4          root       600        524288     2          dest
0x000181cd 5          root       660        4          0
0x000181cb 6          root       666        4          0
0x000f1206 14         root       666        4          0

 

key값을 잘 보시기 바랍니다. 0x000f1206은 10진수로 987654, 즉 우리가 위 코드에 설정한 key값입니다.

 

저희는 프로그램이 종료되면 공유메모리도 종료시키고 싶습니다. 가령 아주 위험하게 이 공유메모리에 민감한 정보라도 있다면 큰일이잖아요? www.poxxhub.com/post_id=xxxxx

 

 

 

다음과 같이 코드를 고쳐봅시다. 

#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(){
        int shmid;
        int *num;
        key_t key=987654;
        void *memory_segment=NULL;

        if((shmid=shmget(key,sizeof(int),IPC_CREAT|0666))==-1){
                printf("shmget failed\n");
                exit(0);
        }


        if((memory_segment=shmat(shmid,NULL,0))==(void*)-1){
                printf("shmat failed\n");
                exit(0);
        }

        num=(int*)memory_segment;
        (*num)++;
        printf("shared memory value :%d\n",(*num));
        if(shmctl(shmid,IPC_RMID,NULL)==-1){
                printf("shmctl failed\n");
        }

        return 0;
}

 

위 코드에 추가한것은 shmctl을 추가한것 밖에 없습니다. cmd로 IPC_RMID를 전달했네요. 그렇다면 이제 이 shmid는 사용할 수 없게 됩니다.

이제 컴파일하고 실행해봅시다.

# gcc shm.c
# ./a.out
shared memory value :5
# ./a.out
shared memory value :1
# ./a.out
shared memory value :1
# ./a.out
shared memory value :1
# ./a.out
shared memory value :1
# ./a.out
shared memory value :1

 

다시 ipcs -m 명령으로 공유메모리가 남아있는지 확인해보면 사라진것을 볼 수 있습니다.

# ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 4          root       600        524288     2          dest
0x000181cd 5          root       660        4          0
0x000181cb 6          root       666        4          0

 

여기까지 공유메모리에 대해서 살펴보았는데요. IPC기법중에서도 그렇게 어렵지 않는 공유메모리를 사용할때 주의해야할 한 가지는 데이터가 오염되지 않게 처리하는 방법입니다. semaphore이든 mutex이든 메모리를 동시에 접근할때 처리를 해주어야하며 이 포스팅에서는 그러한 처리는 하지 않았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

세마포어와 더불어 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

세마포어(Semaphore)란

세마포어(Semaphore)는 사전적 의미는 수기 신호라고 합니다. 다익스트라라는 학자가 고안해낸 이 기법은 두 개 이상의 프로세스가 동시에 공유 메모리와 같은 공유 자원을 접근할때 동기화를 걸어주는 것을 목표로 합니다.

 

다음과 같은 상황을 보도록 합시다.

 

프로세스 A, 프로세스 B는 int a=10이라는 자원을 공유합니다. 사용자는 두 프로세스에서 a를 1씩 증가시키는 작업을 시켰고 사용자는 a가 12라는 값이 될 것을 예상하고 있습니다.

a=10
1) 프로세스 A는 a의 값을 읽어온다. a=10
2) 프로세스 B는 a의 값을 읽어온다. a=10
3) 프로세스 A는 a의 값을 증가시킨다. a=11
4) 프로세스 B는 a의 값을 증가시킨다. a=11
a=11 

이렇게 사용자가 예상했던 것과는 다른 예기치못한 결과를 초래합니다. 이런 결과가 된 이유는 무엇일까요?

우선 a는 두 프로세스에서 접근이 가능한 공유자원입니다. 이것을 동시에 임계영역(Critical section)에 진입하여 공유자원에 접근할때 문제가 발생한게 그 원인이지요. 이처럼 한번에 여러 프로세스가 접근하여 데이터를 동시에 변경하는 것을 막기 위한 장치가 바로 세마포어라고합니다.

임계 영역(critical section)
공유자원에 접근할 수 있는 영역을 말하게 됩니다. 이런 임계 영역은 반드시 보호되어야하는 구간으로 보호 메카니즘으로는 세마포어와 뮤텍스 등이 있습니다. 

세마포어는 P연산, V연산으로 이루어져있으며 지금부터 P,V연산이 무엇인지 아주 간단하게 정의하면 아래와 같습니다.

P : S를 1 감소
V : S를 1 증가

 

이 연산을 이용해 임계영역에 어떻게 동기적으로 진입할 수 있을까요?

우선 S를 1로 생각해보고 프로세스는 S가 1일때만 임계영역으로 진입할 수 있다고 보겠습니다. 그렇다면 S가 0이면 진입하지 못하겠군요. 그렇다면 아래와 같이 임계영역에 접근할 수 있겠네요.

초기 S=1
P(S) (S가 1감소되어 0)
//critical section start
// ...공유 자원을 사용할 수 있는 영역
//critical section end
V(S) (S가 1증가되어 1)

P(S)를 수행하면 S가 1이 감소되어 0이지요. 만약 다른 프로세스가 이 임계영역을 보고 S가 0이면 대기합니다. 그러니까 P연산에는 S가 0이라면 대기하는 코드가 들어가겠네요. 만일 0이 아니라면, 즉 1보다 크다면 S를 1감소시킨 후에 임계영역으로 들어갈 수 있게됩니다.P(S)는 어떻게 구현되어있을까요?

P(S){
    while(S==0){
         //wait
    }
    S--;
}

 

그렇다면 V(S)는 어떻게 구현되어있을까요?

V(S)는 S를 하나 증가시킨다고 했지요? 그렇게함으로써 P(S)에서 기다리고 있는 프로세스가 S가 1이 되는 순간 진입할 수 있게 해주거든요. 대략  아래와 같은 코드로 구현되어있겠네요.

V(S){
    S++;
}

** 오해하지 마세요! 세마포어는 실제 완전히 저렇게 생기지 않습니다. 단순한 이해를 위해서 저런 가상의 코드를 구현해놓은 것입니다. 

 

우리는 여기서 S에 주목할 필요가 있습니다. 만일 S가 1이라면 임계영역에 들어갈 수 있는 프로세스는 하나가 되죠. 그렇다면 S가 2이면 임계영역에 들어갈 수 있는 프로세스는 2개가 된다는 의미겠네요.

 

이렇게 Mutex와 Semephore가 차이가 생겨나게 됩니다. 

Mutex Semaphore
lock, unlock의 상태만 존재하는 일종의 binary semaphore 여러개의 프로세스가 동시에 공유자원에 접근할 수도 있음

 

 

Semapore 시스템콜

다음의 함수를 사용하기에 앞서 우리가 필요한 헤더파일은 다음과 같이 3개입니다.

 #include <sys/types.h>
 #include <sys/ipc.h>
 #include <sys/sem.h>

 

1. semget : semid라는 세마포어 식별자를 얻는데 쓰이는 시스템콜입니다. 세마포어는 집합이라 같은 집합에 속하여 index로 구분됩니다. 보통은 한 집합에 1개의 세마포어를 사용합니다.

int semget(key_t key, int nsems, int semflg);

- key : 세마포어를 식별하는 키입니다.

- nsems :  세마포어 자원의 갯수를 의미합니다.

- semflg : 세마포어 동작옵션인데요. IPC_CREAT과 IPC_EXCL 두개가 존재합니다. 

IPC_CREAT: 새로운 세마포어를 만듭니다. 

IPC_EXCL : IPC_CREAT과 같이 사용하는데, 이미 세마포어가 존재할 경우 Error를 반환합니다.

 

호출 성공시 semid라는 세마포어 식별자를 반환합니다.

 

2. semctl : 세마포어를 제어할 수 있는 시스템 콜입니다.

int semctl(int semid, int semnum, int cmd, ...);

- semid: 세마포어의 식별자입니다. 이는 위의 semget으로부터 나온 id값입니다.

- semnum : semaphore 집합에서 표현되는 일종의 인덱스입니다.

- cmd : 세마포어를 제어할 수 있는 command인데요. 이것에 따라 semctl이 3개의 인자를 갖느냐, 4개의 인자를 갖느냐가 결정됩니다. 

- union semun : cmd에 의해 4번째 인자가 쓰일때 여러분이 작성하는 프로그램에서는 아래의 union을 정의해주어야합니다. 

 union semun {
        int  val;    /* Value for SETVAL */
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
        unsigned short  *array;  /* Array for GETALL, SETALL */
        struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) */
  };

 

- struct semid_ds : semun에 멤버 semid_ds라는 구조체는 <sym/sem.h>에 아래와 같이 정의되어있습니다.

struct semid_ds {
       struct ipc_perm sem_perm;  /* Ownership and permissions */
       time_t sem_otime; /* Last semop time */
       time_t sem_ctime; /* Last change time */
       unsigned long   sem_nsems; /* No. of semaphores in set */
};

 

3. semop : 세마포어의 값을 증가, 감소시킴으로써 크리티컬 섹션 전 후에 사용됩니다.

int semop(int semid, struct sembuf *spos, size_t nsops);

 

- semid : 역시 semget에서 받은 semid를 사용합니다.

- spos : sembuf라는 구조체 포인터네요. sembuf는 어떻게 생겼을까요?

unsigned short sem_num;  /* semaphore number */
 short sem_op;   /* semaphore operation */
 short sem_flg;  /* operation flags */

 

3가지 필드로 구성되어있으며 sem_num은 세마포어 번호, sem_op는 증감값이며 이는 원자적으로 처리됩니다. sem_flg는 옵션입니다.

원자적연산 : 원자는 물체를 더 이상 쪼갤수 없는 단위입니다. 컴퓨터 연산의 원자적이라함은 더 이상 쪼개지지 않는 연산을 의미하여 한 사이클에 동작하는 연산을 의미합니다. a++이라는 연산은 메모리에 1) a의 값을 읽어들이고, 2)a의 값을 1개 증가시키고, 3) a를 다시 메모리에 저장하는 과정을 거치는데 이때 1), 2), 3) 각각이 원자적 연산이며 a++자체는 원자적 연산이 되지 않습니다. 만일 1), 2), 3)의 과정에서 어떠한 방해도 받지 않고 수행할 수 있다면 1)2)3)은 a++은 원자적 연산이라고 할 수 있습니다.

  

이제 시스템콜은 적당히 본것같고 예제를 보도록 합시다.

 

우선 세마포어가 적용되지 않은 쓰레드 2개를 돌리는 예제입니다.

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <stdlib.h>
#include <pthread.h>

int sharedVal=0;
int semid;

void s_wait(){
        struct sembuf buf;
        buf.sem_num=0;
        buf.sem_op=-1;
        buf.sem_flg=SEM_UNDO;

        if(semop(semid,&buf,1) == -1){
                printf("semop() fail\n");
                exit(0);
        }
}

void s_quit(){
        struct sembuf buf;
        buf.sem_num=0;
        buf.sem_op=1;
        buf.sem_flg=SEM_UNDO;

        if(semop(semid,&buf,1) == -1){
                printf("semop() fail\n");
                exit(0);
        }
}

void *thread1_func(void *arg){
        int i;
        for(i=0;i<1000000;i++){
                //s_wait();
                sharedVal++;
                //s_quit();
        }
}


void *thread2_func(void *arg){

        int i;
        for(i=0;i<1000000;i++){
                //s_wait();
                sharedVal++;
                //s_quit();
        }
}

union semun{
        int val;
        struct semid_ds *buf;
        unsigned short *array;
};

int main(int argc, char *argv[]){
        pthread_t thread1;
        pthread_t thread2;
        union semun su;

        if((semid = semget(IPC_PRIVATE, 1, IPC_CREAT|0660)) == -1){
                printf("semet() fail \n");
                exit(0);
        }

        su.val=1;
        if(semctl(semid,0,SETVAL, su) == -1){
                printf("semctl fail\n");
                exit(0);
        }

        pthread_create(&thread1,NULL,thread1_func,NULL);
        pthread_create(&thread2,NULL,thread2_func,NULL);
        pthread_join(thread1,NULL);
        pthread_join(thread2,NULL);

        printf("shared val:%d\n",sharedVal);
        if(semctl(semid, 0, IPC_RMID, su) == -1){
                printf("semctl() fail\n");
                exit(0);
        }
        return 0;
}

 

 

이 코드를 컴파일하려면 gcc [파일명] -lpthread로 컴파일 하시기바랍니다.

저의 컴퓨터에서 실행한 결과 매번 다른 값을 보이고 있습니다. (머신마다 다르니 여러번 시도하거나 for loop을 더 많이 돌려보세요.) 우리가 예상하는 값은 가장 아래의 값입니다.

# ./a.out
shared val:1967612
# ./a.out
shared val:1109699
# ./a.out
shared val:2000000

 

반대로 주석을 풀어 세마포어를 적용시켜보도록 합시다.

저의 컴퓨터에서 실행한 결과 세마포어가 실행하는 시간이 있어 오버헤드가 증가하였으나 우리가 예상하는 값을 얻어낼 수가 있습니다.

# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000

 

예제를 조금 더 살펴보면 다음과 같습니다.

 if((semid = semget(IPC_PRIVATE, 1, IPC_CREAT|0660)) == -1){
        printf("semet() fail \n");
        exit(0);
 }

setget을 통해서 semid를 가져오는데, IPC_PRIVATE를 통해서 1개의 키를 생성하고 있네요. 

 

이후 semum을 통해서 semid를 갖는 세마포어를 제어합니다. 우선 semum의 val을 1로 셋팅합니다. 위의 P(S), V(S) 연산 기억나시나요? 이건 S의 초기값을 설멍하는 것과 같습니다.

이제 semid의 0번 인덱스의 값을 셋팅(SETVAL)하는데, su로 셋팅합니다. 

union semun{
        int val;
        struct semid_ds *buf;
        unsigned short *array;
};

int main(int argc, char *argv[]){
		//..
        union semun su;
		//..
       
        su.val=1;
        if(semctl(semid,0,SETVAL, su) == -1){
                printf("semctl fail\n");
                exit(0);
        }
        //..
 }

 

다음의 s_waits_quit은 임계영역에 들어가기전 기다리는 동작과 나오는 동작을 구현한 함수입니다. 

 

s_wait부터 봅시다. 자, sembuf에 있는 멤버를 들여다보면 semid라는 집합의 (buf.sem_num)0번 인덱스의 동작(buf.sem_op)을 -1로 규정하고 있습니다. 현재 세마포어의 값에 -1연산을 하라는 의미입니다. -2면 2를 빼는거겠죠.

SEM_UNDO는 프로그램이 종료될때 자동적으로 세마포어가 되돌려지는 옵션이라고 하네요. 잘 모르겠습니다 이건.

위의 P연산과 비슷한 역할을 하는 함수가 되겠죠.

 

s_quit은 반대로 sem_id의 0번 인덱스에 1을 더하라는 거겠네요. 그렇다면 위의 V연산과 비슷한 역할을 하는 함수가 됩니다.

void s_wait(){
        struct sembuf buf;
        buf.sem_num=0;
        buf.sem_op=-1;
        buf.sem_flg=SEM_UNDO;

        if(semop(semid,&buf,1) == -1){
                printf("semop() fail\n");
                exit(0);
        }
}

void s_quit(){
        struct sembuf buf;
        buf.sem_num=0;
        buf.sem_op=1;
        buf.sem_flg=SEM_UNDO;

        if(semop(semid,&buf,1) == -1){
                printf("semop() fail\n");
                exit(0);
        }
}

 

마지막으로 쓰레드가 모두 종료되고 프로그램이 종료되기 전에 semctl로 semid를 파기시킵니다. 

if(semctl(semid, 0, IPC_RMID, su) == -1){
         printf("semctl() fail\n");
         exit(0);
}

여기까지 세마포어의 개념과 간단한 예제를 보았습니다. 자세한것은 저도 잘 모르는 입장이라 공부하면서 더 보완을 해야겠네요.

 

읽어주셔서 감사합니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

find

find 명령어는 리눅스를 사용함에 있어서 아주 유용한 기능을 제공해주는 명령어입니다. 말 그대로 우리가 원하는 파일이나 디렉토리를 찾아줍니다. 윈도우로 따진다면 아래의 입력란이 되겠지요.

윈도즈에서는 아래에 파일명을 치면 검색이 가능하지만 리눅스에서는 find라는 명령어를 통해서 파일을 찾을 수 있습니다. 잘만 사용하면 너무나 편리한 명령어이기도 합니다.

하지만 find명령은 너무나 사용하기에 광범위한 명령어이므로 그 중 자주 사용되는 몇가지만 소개하도록 하겠습니다.

 

이제부터 find 명령을 통해서 어떻게 파일을 찾을 수 있을지 알아보겠습니다.

 

1) 기본적인 파일 찾기

find는 기본적으로 찾을 파일 경로와 이름으로 구성되면 정말 그 파일만 찾는 기능을 할 수 있습니다. 아래 처럼 말이지요.

#find /home -name "test.c" 

설명

/home : 파일을 찾을 디렉토리를 지칭합니다.

-name "test.c" : test.c 파일을 찾습니다. 

 

결과

/home/centos/code/test.c
/home/centos/test.c

 

2) 와일드카드(*)를 이용한 단어를 포함한 파일 찾기

find 명령어는 파일이름에 와일드카드 문자(*)를 포함하여 특정 단어를 포함하는 파일을 찾을 수 있습니다.

#find /home -name "*test*"

설명

/home : 파일을 찾을 디렉토리입니다.

-name "*test*" : test라는 단어를 포함하는 파일을 찾습니다. ttest이든, test.c이든, aatestbb 파일이든 전부 찾아줍니다.

 

결과

/home/centos/code/test.c
/home/centos/test.c
/home/centos/patience_test.c

 

3) -type 옵션으로 파일의 타입을 포함하여 찾기

저는 test라는 디렉터리를 하나 만들었고 여기서 test라는 단어를 포함하는 디렉터리를 찾아보도록 하겠습니다.

/home/centos/code/test.c
/home/centos/test.c
/home/centos/patience_test.c
/home/centos/test <- directory
#find /home -name "*test*" -type d

설명 

-type d : 파일 type이 디렉터리인것만 찾습니다.

 

결과

/home/centos/test

파일 타입은 다음과 같습니다.

option type
f regular file
d directory
c character special
b block special
l symbolic link
s socket
p fifo

 

4) 파일의 크기에 따른 파일 검색

-size 옵션으로 파일의 크기를 검색할 수 있습니다. 파일 크기 앞에 '+'를 붙이면 그 크기 초과, '-'를 붙이면 그 크기 미만으로 검색이 됩니다. 또한 파일 크기 뒤에 단위가 붙는데요. 아래와 같습니다. 단위를 생략하면 기본적으로 리눅스 블록(b)이 단위가 됩니다.

(b:block, c:bytes, w:2bytes, k:kbytes, M:mbytes, G:gbytes)

옵션 단위
c 1byte 단위
b 1block 단위(1block = 512 bytes)
w 2 bytes 단위
k 1 kilobytes
M 1 metabytes
G 1 gigabytes

몇 가지 예제)

find /home -size 1k 크기가 1k인 파일 검색
find /home -size +100M 100M 초과인 파일 검색
find /home -size -1G 1G 미만인 파일 검색
find /home -size +100M -size -2G 100M 초과 2G 미만인 파일 검색

 

 

5) exec으로 찾을 파일에 명령어 실행

-exec으로 명령어를 실행할 수 있습니다.

find명령을 통한 파일들은 모두 {}에 담겨지게 됩니다. 그러니까 ls -al {} 또는 ls {} -al 인 것이고, cp {} . 인것이 왜인지 생각해보세요.

ls -al {파일}, 또는 ls {파일} -al은 같은 결과를 갖습니다. 전달받는 파일 인자는 1개이니까요 .

반대로 cp는 2개의 파일 인자를 받습니다. 첫번째 인자는 복사가 될 파일, 두번째 인자는 복사가 될 위치이지요. 그래서 cp {파일} . 가 됩니다.

명령어 끝은 항상 \;으로 끝나야합니다. 프로그래밍을 하는 분이라면 ;이 연산의 종료를 의미한다는 것을 아실겁니다. ';'은 특수문자이기 때문에 escape(특수문자화 하지 않는 것)하기 위해 \를 같이 사용합니다. 이해가 안된다면 그냥 외우시거나 프로그래밍 언어를 배우시는 것을 추천드립니다. ㅎㅎㅎ 

 

예제를 통해서 살피도록 하겠습니다.

 

예제 1) 

find /home -name "*test*" -exec ls -al {} \;

설명

test라는 단어가 포함된 파일을 대상으로 ls -al을 실행합니다. {}의 위치를 잘봐두세요.

 

결과

-rw-r--r--. 1 root root 313  2월 29 02:37 /home/centos/code/test.c
-rw-r--r--. 1 root root 0  3월  3 06:57 /home/centos/test.c
-rw-r--r--. 1 root root 0  3월  3 07:04 /home/centos/patience_test.c
합계 4
drwxr-xr-x.  2 root   root      6  3월  3 07:08 .
drwx------. 17 centos centos 4096  3월  3 07:34 ..

 

예제 2)

find /bin/ -name "*gr*" -exec cp {} /home/centos  \;

설명

bin 하위의 gr이라는 파일이나 디렉토리를 복사해 /home/centos로 위치합니다. 실제 실행하고 나서 /home/centos에 위치하여 결과를 보면 이런 파일들이 존재합니다.

 

결과

bzegrep            groff                  grub2-mkstandalone  xzfgrep
bzfgrep            grops                  grub2-script-check  xzgrep
bzgrep             grotty                 grub2-syslinux2cfg  zegrep
chgrp              groups                 lexgrog             zfgrep

..생략..

이뿐만 아니라 rm, mv 등의 명령도 실행 가능합니다. rm은 쓸때 꼭 주의하세요.

 

예제 3)

아까 옮겼던 gr이 포함된 단어의 파일들을 전부 삭제해보도록 하겠습니다.(우선 그전에 gr이 포함된 파일 중 원래 그 자리에 있었던 파일이 있는지 확인하세요.)

find /home/centos/ -name "*gr*" -exec rm -rf {} \;

설명

찾은 파일에 대해서 강제 삭제 명령(rm -rf)을 하게 됩니다. 이 명령어는 전달되는 파일이 하나밖에 없으므로 {}이 어디에나 위치해도 됩니다. rm -rf {} 이건 rm {} -rf이건 상관이 없다 이거죠. 허나 반드시 이 명령어를 실행하지 마시기바랍니다. 잘못하면 진짜 ㅈ돼요.

 

결과

gr이 포함된 모든 파일이 삭제되었습니다.

 

6) 특정 단어 또는 내용을 갖는 파일을 검색하기

끝에 파이프를 연결해서 | xargs grep "검색할 내용"을 덧붙이면 되는데 에러 또는 경고까지 전부 출력하므로 에러(2)는 전부 쓰레기통(/dev/null)에 버려 출력하지 않게 합시다.

find / -name "*.c*" -type f | xargs grep "#include" 2>/dev/null

설명

/디렉토리부터 확장자가 c를 포함하며 파일은 정규파일을 검색합니다.  그때 #include라는 문자열을 포함하는 파일만 검색하며 오류 출력은 전부 쓰레기통인 /dev/null로 갖다 버립니다.

 

결과

뭐 이런것들이 보이네요.

/usr/lib/firmware/isci/create_fw.c:#include 
/usr/lib/firmware/isci/create_fw.c:#include 
/usr/lib/firmware/isci/create_fw.c:#include 
/usr/lib/firmware/isci/create_fw.c:#include <sys/types.h>
/usr/lib/firmware/isci/create_fw.c:#include <sys/stat.h>
/usr/lib/firmware/isci/create_fw.c:#include 
/usr/lib/firmware/isci/create_fw.c:#include 
/usr/lib/firmware/isci/create_fw.c:#include 
/usr/lib/firmware/isci/create_fw.c:#include <asm/types.h>
/usr/lib/firmware/isci/create_fw.c:#include 

... 생략 ...

 

 

 

7) 특정 권한을 갖는 파일 찾기

-perm 옵션으로 어떤 파일이 어떤 권한이 있는지 검색할 수 있습니다. 

find . -perm 777

설명

위의 명렁은 유저 권한으로 읽기, 쓰기 , 실행 그리고 그룹 권한으로 읽기, 쓰기, 실행 마지막으로 다른 유저 권한으로는 읽기, 쓰기, 실행 권한인 파일을 모조리 찾는 명령어입니다.

만일 -perm 644라면 유저 권한으로 읽기,쓰기 그룹 권한으로 읽기, 다른 유저 권한으로 읽기의 파일들을 전부 찾습니다.

 

8) 끝까지 찾지 않아도 되는 maxdepth

maxdepth는 찾는 깊이를 뜻합니다. find명령어는 기본적으로 지정된 디렉토리 밑으로 끝까지 search합니다. 만약 maxdepth 옵션을 준다면 지정된 깊이까지만 파일을 검색합니다. 깊이라함은 하위디렉토리를 뜻합니다.

maxdepth의 0은 자기 자신을 의미합니다.

 find / -maxdepth 1

설명

/디렉토리에서 깊이가 1인 파일들만 검색합니다. 결과는 다음과 같습니다.

 

결과

/
/dev
/proc
/run
/swap
/sys
/etc
/root
/var
/usr
/bin
/sbin
/lib
/lib64
/boot
/home
/media
/mnt
/opt
/srv
/tmp

 

지금까지 제가 현업에서 자주 사용하는 find명령어의 옵션을 살펴보았는데요. 가만히보면 이외의 명령어를 사용할 일이 별로 없었습니다. 다른 분들은 잘 모르겠네요. 

 

기능도 막강하고 배우기에는 너무 범위가 넓은 find 명령어는 알면 알 수록 흥미있는 명령어인것 같습니다. 제가 모르거나 놓친 부분이거나, 또는 많이 사용하지만 이곳에 나오지 않은 옵션이라면 댓글 달아주세요!

 

나머지 기능은 꾸준히 업데이트하여 올리도록 하겠습니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

소켓 통신에 대한 개념과 예제가 더 많은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

소켓(socket)

네트워크 통신을 하는 표준 방법으로 프로세스간 연결의 종점이라고 볼 수 있습니다. 기본적인 개념은 아래의 그림과 같습니다. 

위의 그림은 TCP/IP에서의 인터넷 통신을 보여줍니다. 클라이언트의 컴퓨터의 물리적 주소(MAC 주소) DD-44-EE-55-FF-66이며 논리적 주소(IP 주소)는 10.2.2.2입니다. 클라이언트는 여러가지의 프로그램을 실행시키고 있는데 그 중 어떤 프로세스는 TCP 포트번호 12345를 사용합니다. 이 클라이언트 프로세스는 물리적 주소가 11-AA-22-BB-33-CC이며 논리적 주소 10.1.1.3인 서버 컴퓨터의 포트 번호 80번을 사용하는 서버 프로세스와 연결되어 있습니다.

 

구체적으로 어떻게 통신할까요? 각 프로세스는 소켓을 통해서 통신을 하게 되는데, 소켓은 간단히 얘기해 ip주소와 포트번호를 갖고 있는 인터페이스라고 생각하면 됩니다. 소켓은 리눅스에서 파일로 다루어지며 프로세스는 이 소켓을 사용할때 파일디스크립터를 통해 사용합니다. 우리는 리눅스 파일 입출력에 대해 배울때 파일디스크립터를 사용했지요? 소켓 역시 파일디스크립터를 이용해서 읽기, 쓰기가 가능합니다.

 

소켓 통신할때 필요한 주요함수는 무엇이 있을까요? 간단히 알아보도록 합시다. 

 

1. socket(int domain, int type, int protocol)

소켓을 만드는데 바로 이 함수를 사용합니다. 소켓 역시 파일로 다루어지기 때문에 반환값은 파일디스크립터입니다. 만약 소켓을 여는데 실패했다면 -1을 리턴합니다.

 

2. connect(int fd, struct sockaddr *remote_host, socklen_t addr_length)

원격 호스트(원격 컴퓨터)와 연결하는 함수입니다. 연결된 정보는 remote_host에 저장됩니다. 성공시 0, 오류시 -1을 반환합니다.

 

3. bind(int fd, struct sockaddr *local_addr, socklen_t addr_length)

소켓을 바인딩합니다. 이렇게 생각하면 됩니다. 지금 fd로 넘겨지는 소켓과 이 프로세스와 묶는다(bind)라고 생각하시면 됩니다. 그래서 해당 프로세스는 소켓을 통해 다른 컴퓨터로부터 연결을 받아들일 수 있습니다.

 

4. listen(int fd, int backlog_queue_size)

소켓을 통해 들어오는 연결을 듣습니다. backlog_queue_size만큼 연결 요청을 큐에 넣습니다. 성공시 0, 오류시 -1을 반환합니다.

 

5. accept(int fd, sockaddr *remote_host, socklen_t *addr_length)

어떤 컴퓨터에서 이 컴퓨터로 연결할때 연결을 받아들입니다. 함수 이름이 말해주고 있죠.

연결된 원격 컴퓨터의 정보는 remote_host에 저장됩니다. 오류시에 -1을 반환합니다.

 

6. send(int fd, void* buffer, size_t n, int flags)

buffer를 소켓 파일 디스크립터인 fd로 전송합니다. 보낸 바이트수를 반환하며 실패시 -1을 반환합니다.

 

7. recv(int fd, void* buffer, size_t n, int flags)

send함수와 사용법이 거의 비슷합니다. n바이트를 buffer로 읽습니다. 성공시 받은 바이트수를 반환하며 실패시 -1을 반환합니다.

 

 

 

이제 예제를 보며 더 자세한 설명을 하도록 하죠. 다음 예제는 서버 프로그램이 클라이언트 프로그램에서 전송한 메시지를 출력해주는 소스코드입니다. 클라이언트 프로그램은 텔넷을 사용할 것이기 때문에 따로 클라이언트 프로그램 소스코드는 없습니다.

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 12346
#define BUF_SIZE 1024
int main(void){
        int socket_fd,accepted_fd;
        struct sockaddr_in host_addr, client_addr;
        socklen_t size;
        int recv_length;
        char buffer[BUF_SIZE];

        socket_fd=socket(PF_INET,SOCK_STREAM,0);

        host_addr.sin_family=AF_INET;
        host_addr.sin_port=htons(PORT);
        host_addr.sin_addr.s_addr=0;
        memset(&(host_addr.sin_zero),0,8);

        bind(socket_fd,(struct sockaddr *)&host_addr,sizeof(struct sockaddr));

        listen(socket_fd,3);

        while(1){
                size=sizeof(struct sockaddr_in);
                accepted_fd=accept(socket_fd,(struct sockaddr *)&client_addr,&size);

                send(accepted_fd,"Connected",10,0);
                printf("Client Info : IP %s, Port %d\n", inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));

                recv_length=recv(accepted_fd,&buffer,BUF_SIZE,0);
                while(recv_length>0){
                        printf("From Client : %s\n",buffer);
                        recv_length=recv(accepted_fd,&buffer,BUF_SIZE,0);
                }

                close(accepted_fd);
        }
        return 0;
}

 

여기서 포트번호는 12346을 사용한다고 하겠습니다.

 

socket_fd=socket(PF_INET,SOCK_STREAM,0);

socket의 첫번째 인자는 프로토콜 체계 PF(Protocol Family)를 지정합니다. PF_INET은 인터넷 IP프로토콜 체계입니다. 사용하는 프로토콜 체계에는 여러가지가 있습니다. IP외에도 1970년대에 개발된 공중 데이터 네트워크에 대한 표준 X.25 외에도 애플토크,XEROX 네트워크 등등 있는데 우리는 IP를 사용할 것이기 때문에 PF_INET만 사용할 것입니다.

 

두번째 인자는 소켓의 타입입니다. 가장 보편적으로 사용하는 타입은 Stream과 Datagram입니다. SOCK_STREAM은 연결형, SOCK_DGRAM은 비연결형이라고 생각하면 되겠습니다.

 

세번째 인자는 프로토콜로, 일반적으로 0을 넣어주면 시스템이 자동으로 설정해줍니다.

host_addr.sin_family=AF_INET;
host_addr.sin_port=htons(PORT);
host_addr.sin_addr.s_addr=0;
memset(&(host_addr.sin_zero),0,8);

bind(socket_fd,(struct sockaddr *)&host_addr,sizeof(struct sockaddr));

 

다음은 바인드할때 구조체를 넘겨야하는데요. 이 프로세스가 사용할 소켓 fd와 컴퓨터의 IP주소, 포트와 묶는 작업이라고 보면 됩니다.

 

우리는 TCP/IP 상에서의 통신이기 때문에 IPv4용 구조체인 sockaddr_in을 사용합니다.

sin_family에는 IP용 Address Family(AF_INET)을 지정합니다. 

sin_port는 이 프로세스가 사용할 포트번호를 지정합니다.

sin_addr.s_addr에는 주소가 들어가게 되는데요. 0은 현재 컴퓨터의 주소를 자동으로 채우라는 의미입니다. 그것이 아니라면 주소를 직접 지정해주어야합니다.

 

htons?

sin_port에서 htons는 무슨 함수일까요? 이 함수의 풀 네임은 host-to-network short로 16비트 정수를 호스트 바이트 순서에서 네트워크 바이트 순서로 변환하는 함수입니다.

AF_INET 소켓 주소 구조체에서 사용되는 포트 번호와 IP주소는 빅 엔디언(Big-Endian)이라는 네트워크 바이트 순서를 따릅니다. 이것은 보통 우리가 사용하는 x86의 리틀 엔디언과는 반대의 표기법이죠. 그래서 변환없이 그대로 사용하게 되면 바이트 순서가 달라지게 되어 제대로 동작하지 않습니다.

 

이제 bind를 호출하는데 두번째 인자를 보세요. (struct sockaddr*)로 형 변환하고 있습니다. host_addr이라는 구조체 변수는 sockaddr_in이라는 구조체입니다.

bind는 TCP/IP뿐만 아니라 X.25, 애플토크 등 여러 프로토콜이 존재하기 때문에 인자로 받아야할 구조체 형이 sockaddr_in뿐만이 아닙니다. 그래서 일반화된 구조체가 필요하게 되는데 그 구조체가 sockaddr입니다.

 

sockaddr_in은 sockaddr로 형변환할 수 있습니다. 왜냐하면 구조체의 크기가 같기 때문이죠. sockaddr 구조체는 2바이트의 Address Family와 14바이트의 주소를 사용합니다. 반면 sockaddr_in의 IPv4 전용 구조체는 sa_data의 주소를 포트번호, ip주소, 기타 추가 비트를 포함하고 있죠.  우리는 일반화된 sockaddr에 sa_data에 직접 포트번호와 주소를 읽고 쓰기가 상당히 불편합니다.

그래서 더 사용하기 편한 인터넷 전용 구조체를 사용합니다. 형변환에 문제가 없게 크기를 같게 만들어 호환성에 문제가 없습니다.

 

listen(socket_fd,3);

 

그 소켓으로 들어오는 연결을 기다립니다. 마지막인자는 백로그 큐의 최대크기입니다.

while(1){
	size=sizeof(struct sockaddr_in);
	accepted_fd=accept(socket_fd,(struct sockaddr *)&client_addr,&size);

	send(accepted_fd,"Connected",10,0);
	printf("Client Info : IP %s, Port %d\n", inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));

	recv_length=recv(accepted_fd,&buffer,BUF_SIZE,0);
	while(recv_length>0){
		printf("From Client : %s\n",buffer);
		recv_length=recv(accepted_fd,&buffer,BUF_SIZE,0);
	}

	close(accepted_fd);
}
        

 

 

 

accept를 통해서 이제 새롭게 연결된 클라이언트 전용 파일디스크립터를 얻어옵니다. 클라이언트에 대한 정보는 client_addr에 저장됩니다.

 

연결이 성공돼었다면 send를 통해서 "Connected"라는 문자열을 클라이언트 쪽으로 보냅니다.

그 후 클라이언트의 정보를 출력하지요. 두가지를 출력합니다. IP주소와 Port번호입니다.

 

inet_ntoa(struct in_addr *network_addr)

네트워크 주소를 숫자사이의 점을 찍는 IP주소로 network to acsii라는 뜻입니다. ip주소가 담긴 in_addr구조체는 32비트의 네트워크 주소를 갖고 있기 때문에 숫자사이에 점을 찍는 형태로 바꾸려면 이 함수를 사용합니다.

 

ntohs(Network-to-Host Short)

이것 역시 네트워크 바이트 순서가 빅 엔디안이고, 호스트의 바이트 순서가 리틀 엔디안일때 변환해야할때 사용합니다.

 

그 후에는 계속 recv를 통해 클라이언트에서 입력받은 메시지를 서버에서 그대로 출력해줍니다.

 

결과

서버

# gcc server.c
# ./a.out

 

클라이언트

# telnet 192.168.10.131 12348
Trying 192.168.10.131...
Connected to 192.168.10.131.
Escape character is '^]'.

Connected

 

서버

Client Info : IP 192.168.10.131, Port 43774

 

 

클라이언트

Hi
I'm Reakwon

 

서버

From Client :

From Client : Hi

From Client : I'm Reakwon

 

클라이언트에서 타이핑한 것이 서버에서 그대로 출력이 되는 것을 볼 수 있습니다.

 

이상으로 간단히 리눅스의 소켓과 그에 대한 예제를 보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

파이프와 더불어 더 많은 정보를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

파이프(Pipe)

파이프(Pipe)란 프로세스간 통신을 할때 사용하는 커뮤니케이션의 한 방법입니다. 가장 오래된 UNIX 시스템의 IPC로 모든 유닉스 시스템이 제공합니다. 하지만 두가지 정도의 한계점이 있습니다.

 

첫번째 한계점으로 파이프는 기본적으로 반이중 방식입니다. 물론 전이중 방식을 지원하는 시스템이 있긴 하나, 최대의 이식성을 위해서는 파이프는 반이중 방식이라는 생각을 해야합니다. 이것은 FIFO라는 명명된 파이프로 극복할 수 있습니다.

두번째 한계점으로는 부모, 자식 관계에서의 프로세스들만 사용할 수 있습니다. 부모프로세스가 파이프를 생성하고, 이후 자식 프로세스와 부모프로세스가 파이프를 이용하여 통신합니다.

 

이러한 한계점이 잇긴 하지만 여전히 쓸모있는 IPC기법입니다.

 

파이프는 unistd.h 헤더파일이 존재합니다.

 

#include <unistd.h>
int pipe(int fd[2]);

pipe함수가 성공적으로 호출되었다면 0, 실패했을 경우 -1을 반환합니다.

인자 fd는 2개의 원소가 있는 배열이라는 점을 주목합시다. 2개의 원소를 쓰는 이유가 있습니다. 아래의 그림을 보면서 이해합시다.

 

 

파이프는 커널영역에 생성되어 파이프를 생성한 프로세스는 파일 디스크립터만 갖고 있게 됩니다. 여기서 파일디스크립터 fd[1]은 쓰기용 파이프, fd[0]은 읽기용 파이프입니다. 그러니 우리가 만약 데이터를 fd[1]에 쓰게 되면 fd[0]으로 그 데이터를 읽을 수 있는 것입니다.

 

그렇다면 자식 프로세스를 하나 더 두어서 자식과 부모가 통신할 수 있게 하려면 어떻게 해야할까요? 우선 자식 프로세스를 fork하면 파일 디스크립터는 부모의 파일디스크립터를 자식이 그대로 사용할 수 있는 것을 활용합니다. (파일디스크립터가 그대로 자식프로세스에 복제됩니다.)

부모프로세스는 파이프에 데이터를 쓰는 프로세스, 자식 프로세스는 그 파이프에서 데이터를 읽는 프로세스로 설계합시다.

 

 

우선 부모 프로세스에서 파이프를 생성하면 파이프에 데이터를 쓸것이기 때문에 읽기 파이프는 닫습니다. fd[0]이죠? 그런 후 fd[1]에 데이터를 씁니다.

자식 프로세스는 쓰기 파이프는 쓰지 않으므로 fd[1]을 닫고, 읽기 파이프로 데이터를 읽습니다.

 

다음은 그런 기능을 하는 코드입니다.

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAX_BUF 1024
#define READ 0
#define WRITE 1
int main(){
        int fd[2];
        pid_t pid;
        char buf[MAX_BUF];

        if(pipe(fd) < 0){
                printf("pipe error\n");
                exit(1);
        }
        if((pid=fork())<0){
                printf("fork error\n");
                exit(1);
        }

        printf("\n");
        if(pid>0){ //parent process
                close(fd[READ]);
                strcpy(buf,"message from parent\n");
                write(fd[WRITE],buf,strlen(buf));
        }else{  //child process
                close(fd[WRITE]);
                read(fd[READ],buf,MAX_BUF);
                printf("child got message : %s\n",buf);
        }
        exit(0);
}

 

결과는 아래와 같습니다.

 

child got message : message from parent

 

자식 프로세스에서 부모 프로세스가 pipe에 쓴 데이터를 읽었습니다. 

또는 자식프로세스가 데이터를 쓰고, 부모프로세스가 데이터를 읽는 설계도 가능하겠죠.

 

 

그렇다면 부모 프로세스와 자식 프로세스가 읽기, 쓰기가 가능하게 구현하려면 어떻게 해야할까요? 파이프를 한개만 사용한다고 해봅시다.

 

그리고 이런 상황을 가정해보지요.

1. 먼저 부모프로세스가 파이프에 fd[1]로 데이터를 보냅니다. 

2. 그 이후 자식 프로세스가 부모 프로세스가 쓴 데이터를 fd[0]으로 읽습니다.

3. 자식 프로세스는 바로 fd[1]로 파이프에 응답값을 보냅니다.

4. 부모 프로세스는 fd[0]으로 자식 프로세스가 보낸 응답값을 읽습니다.

 

결론을 말씀드리면 항상 위의 상황은 발생하지 않습니다. 그 이유는 누가 먼저 파이프를 읽느냐에 따라서 결과가 달라지는데, 만일 부모프로세스가 파이프에 쓰고, 자식 프로세스가 그 데이터를 읽기도 전에 부모프로세스가 먼저 데이터를 읽는다면 파이프에 데이터는 없겠죠. 허나 자식 프로세스는 없는 데이터를 계속 읽기만 기다리고 있기 때문에 프로그램이 망하게 되는 겁니다.

 

이때는 파이프를 2개 사용해야합니다.

fdA와 fdB 2개 사용합니다. 부모프로세스는 자식에게 쓰기용으로 fdA[1], 자식프로세스로부터 읽기용으로 fdB[0]만 있으면 됩니다. 필요없는 fdA[0], fdB[1]은 닫아줍니다.

그리고 자식프로세스는 부모프로세스로부터 읽기용으로 fdA[0], 쓰기용으로 fdB[1]만 있으면 되지요. 역시 필요없는 fdA[1], fdB[0]은 닫아줍니다.

이제 이런 개념으로 코드를 구현합시다.

 

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAX_BUF 1024
#define READ 0
#define WRITE 1
int main(){
        int fdA[2],fdB[2];
        pid_t pid;
        char buf[MAX_BUF];
        int count=0;

        if(pipe(fdA) < 0){
                printf("pipe error\n");
                exit(1);
        }

        if(pipe(fdB) < 0){
                printf("pipe error\n");
                exit(1);
        }

        if((pid=fork())<0){
                printf("fork error\n");
                exit(1);
        }

        printf("\n");
        if(pid>0){ //parent process
                close(fdA[READ]);
                close(fdB[WRITE]);
                while(1){
                        sprintf(buf,"parent %d",count++);
                        write(fdA[WRITE],buf,MAX_BUF);
                        memset(buf,0,sizeof(buf));
                        read(fdB[READ],buf,MAX_BUF);
                        printf("parent got message : %s\n",buf);
                        sleep(1);
                }
        }else{  //child process
                close(fdA[WRITE]);
                close(fdB[READ]);
                count=100000;
                while(1){
                        sprintf(buf,"child %d",count++);
                        write(fdB[WRITE],buf,MAX_BUF);
                        memset(buf,0,sizeof(buf));
                        read(fdA[READ],buf,MAX_BUF);
                        printf("\tchild got message : %s\n",buf);
                        sleep(1);
                }
        }
        exit(0);
}

 

 

부모 프로세스는 0부터 1초마다 증가한 값을 파이프에 쓰고, 자식 프로세스로부터 파이프로 읽습니다. 자식 프로세스는 100000부터 증가한 값을 1초마다 쓰고, 읽습니다. 그 결과는 아래와 같습니다.

        child got message : parent 0
parent got message : child 100000
        child got message : parent 1
parent got message : child 100001
        child got message : parent 2
parent got message : child 100002
        child got message : parent 3
parent got message : child 100003
        child got message : parent 4
parent got message : child 100004
        child got message : parent 5
parent got message : child 100005

       

부모 자식 관계의 프로세스가 아닌 별개의 프로세스가 통신할때는 아까 위에서 말씀드린 것 처럼 FIFO를 사용해야합니다. 

 

파이프를 이용한 IPC구현, 이제 어렵지 않겠죠?

 

반응형
블로그 이미지

REAKWON

와나진짜

,

SSH(Secure Shell)

SSH란 원격에서 서버로 터미널에 접속할때 암호화를 통해 안전한 접속환경을 제공하여 telnet과 같은 안전하지 않은 원격 터미널 접속 프로그램을 보완하는 프로토콜 또는 프로그램입니다. SSH는 기본적으로 포트 22번을 사용합니다.

 

원격에서 telnet으로 서버에 접속할 경우 암호화 과정을 거치지 않으므로 스니핑을 한다면 계정과 비밀번호가 고스란히 노출됩니다. 하지만 ssh를 사용하여 접속한다면 암호화가 되었기 때문에 스니핑을 하여 패킷을 보더라도 의미를 파악할 수 없습니다. 또한 강력한 인증기능까지 지원합니다.

 

SSH의 주요 기능은

  • 보안 접속을 통한 rsh, rcp, rlogin, rexec, telnet, ftp 등을 제공합니다.
  • IP spoofing을 방지하기 위한 기능을 제공합니다.
  • X11 패킷 포워딩 및 일반적인 TCP/IP 패킷 포워딩을 제공합니다.

[출저 : 위키백과]

 

그렇다면 telnet접속과 ssh접속은 어떻게 다른지 눈으로 직접 살펴봅시다.

 

 

telnet으로 서버접속( 클라이언트 IP : 110.13.7.47 , 서버 IP : 110.13.7.20)

window 클라이언트에서 cmd에서 telnet을 통해 서버로 접속합니다.

엔터를 치는 순간 네트워크에서는 어떤 페킷이 오고 갔을까요? 아래는 와이어샤크로 본 페킷들입니다. 이 페킷을 보면 localhost login: 이라는 문자열을 볼 수 있습니다. 

서버(110.13.7.20)에서 클라이언트(110.13.7.47)로 보내는 군요. 그렇다면 클라이언트의 telnet 터미널 화면은 어떻게 됐을까요?

localhost login: 이라는 로그인 화면으로 들어왔습니다. 그렇다면 위의 페킷은 로그인하라는 디스플레이 메시지를 보냈다는 것을 짐작할 수 있겠군요.

 

이제 로그인을 시도해보지요. 아이디는 reakwon, 비밀번호는 reakwon입니다. 여기서 클라이언트(110.13.7.47)에서 서버(110.13.7.20)로 보내는 페킷만을 봅시다.

자, r이라는 문자를 보내는 군요. 이후에 75.6.355213의 페킷을 봅시다. r문자를 보낸 이후의 페킷입니다.

e라는 문자가 찍혔네요. 이렇게 reakwon이라는 문자를 서버에 보내고 서버는 그에 대한 응답으로 다시 reakwon이라는 문자를 각각 echo합니다. 이렇게 유저의 계정은 찾을 수 있습니다.

 

비밀번호 역시 다르지 않습니다. 이후의 페킷을 보면 알 수 있습니다. 다음은 계정의 비밀번호를 알아낼 수 있는 페킷들입니다. 하나씩 보도록 하지요.

페스워드의 시작을 알리는 페킷이 나옵니다. 

이후 아까처럼 reakwon의 문자들을 연속해서 서버에 보냅니다. 비밀번호를 입력할땐 서버로 부터 echo가 오지 않는군요.

이제 망했습니다. 해킹당했군요.

 

 

ssh로 서버접속( 클라이언트 IP : 110.13.7.47 , 서버 IP : 110.13.7.20)

다음은 ssh로 접속한 페킷들입니다. 대충보아도 서버와 클라이언트의 페킷이 암호화되었다고 하는군요. SSH버전은 2입니다.

페킷 하나를 보면 이렇습니다. 음, 암호화된 페킷을 보면 34e96..., 메시지 인증 코드는 14b4... 입니다. 

암호화된 페킷이라 잘 모르겠습니다. 암호화를 하여 데이터를 전송하고 수신하는 것만 알면 되겠습니다.

CentOS7에서 SSH로 서버 접속

CentOS7에서 ssh연결은 다음과 같은 과정을 거치는데요.

1. openssh-server 설치

2. 방화벽 열기

3. 가상머신을 돌릴 경우

 

1. openssh-server 설치

일단 ssh 서버가 설치되어 있어야합니다. 우선 설치되어 있나 확인을 해봅시다. 

 

[root@localhost etc]# rpm -qa | grep openssh-server
openssh-server-7.4p1-16.el7.x86_64

 

이미 설치가 되어있군요. 설치가 안된 분들은 설치하면 됩니다. 

다음의 명령으로 ssh 서버를 설치하세요.

 

yum install openssh-server

 

 

 

 

2-1. 방화벽 포트 열기

CentOS7에서는 기본적으로 방화벽(firewalld)가 설치되어 있습니다. 혹여나 설치가 되어있지 않은 분들은 yum install firewalld 명령으로 방화벽을 설치합시다.

 

ssh서버와 firewalld가 설치가 되었다면 방화벽에 ssh가 사용하는 포트를 열어줘야합니다. 그래서 /etc/ssh/sshd_config 파일을 열어 

(루트 사용자라면 vi /etc/ssh/sshd_config, 아니면 sudo vi /etc/ssh/sshd_config) 

#semanage port -a -t ssh_port_t -p tcp #PORTNUMBER
#
#Port 22    <-- #해제
#AddressFamily any
#ListenAddress 0.0.0.0

 

에서 #Port 22의 주석을 해제합니다. 앞의 #을 지우게 되면 주석이 해제되는 겁니다. 이 후 sshd을 다시 시작해야되겠죠? 다음의 명령어로 sshd를 다시 시작합시다.

 

systemctl restart sshd.service

 

2-2. 방화벽 포트 열기

또는 다른 방법이 있는데요. 다음의 명령으로 방화벽에 명시해줍니다.

 

sudo firewall-cmd --permanent --add-service=ssh

 

이후 firewall의 변경사항을 적용하고 싶다면 

 

sudo firewall-cmd --reload

 

의 명령을 사용한 후 방화벽을 다시 시작해주어야합니다.

 

sudo systemctl restart firewalld

 

3. 혹시 가상머신을 사용해서 ssh를 쓰고 계신가요?

그럴 경우 설정-> 네트워크 -> 어댑터 -> 어댑터에 브릿지를 선택해주세요.

 

 

연결

자, 이제 putty라는 프로그램 또는 mobaXterm이라는 ssh 프로토콜이 가능한 프로그램을 사용해서 연결해봅시다. 일단 저의 ssh 서버의 주소는 ifconfig라는 명령어로 볼 수 있습니다. 

[root@localhost etc]# ifconfig
enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
inet 192.168.10.123  netmask 255.255.255.0  broadcast 192.168.10.255 

...

 

ssh의 서버주소는 192.168.10.123이군요. 이제 putty라는 프로그램으로 여기에 ssh로 접속해봅시다.

 

이제 open을 하게 되면 아래 처럼 로그인 화면이 나옵니다. 혹은 첫번째 실행이라면 뭐 yes, no를 묻는 대화창이 나오게 되는데 yes눌르시고 로그인하면 됩니다.

 

이제 원격에서도 ssh를 통해 서버에 접속 할 수 있습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

pwd (Print Working Directory)

현재 디렉토리를 알려주는 명령어입니다. 현재 유저가 어느 위치에 있는지 알아볼 수 있는 명령어입니다. 

 

아래의 예에서는 현재 작업디렉토리가 /home/reakwon이라는 디렉토리라는 것을 알 수 있습니다.

[reakwon@localhost ~]$ pwd

/home/reakwon

cd (Change Directory)

디렉토리를 이동하는 명령어입니다. 절대 경로나 상대 경로를 주어서 디렉토리를 이동할 수 있습니다. 

 

cd /root  = /root 디렉토리로 이동합니다.

cd ..  = 현재 디렉토리의 상위 디렉토리로 이동합니다.

cd ~ = 현재 사용자의 홈 디렉토리로 이동합니다.

 

이런 조합으로 이동이 가능합니다.

 

 

ls (List)

디렉토리 안의 파일과 디렉토리를 알 수 있는 명령어입니다. 여러가지 옵션이 존재하는데, 옵션은 여러가지를 혼합해서 사용할 수 있습니다. 옵션을 살펴보도록 하지요.

 

a : 모든 파일을 보여줍니다. 즉, 숨김파일까지 전부 보여주는 것이죠. 숨김파일이란 파일명 앞에 "."이 달린 파일명이고 ls명령어만으로 보여지지 않는 파일입니다.

 

l : (소문자 '엘') 파일의 자세한 내용을 보여줍니다. 

다음은 제 리눅스 pc의 tmp 디렉토리의 내용의 일부를 보여줍니다.

-rw-r--r--. 1 root    root    1148  5월 16 14:33 anaconda.log
drwx------. 2 reakwon reakwon   25  5월 16 14:50 firefox_reakwon
drwxr-xr-x. 2 root    root      18  5월 16 13:51 hsperfdata_root
-rw-r--r--. 1 root    root     420  5월 16 14:33 ifcfg.log
-rwx------. 1 root    root     836  5월 16 14:29 ks-script-o30BkB
-rw-r--r--. 1 root    root       0  5월 16 14:32 packaging.log
-rw-r--r--. 1 root    root       0  5월 16 14:32 program.log
-rw-r--r--. 1 root    root       0  5월 16 14:32 sensitive-info.log
drwx------. 2 reakwon reakwon   24  5월 16 14:34 ssh-TF6BwHbO3Rdo
drwx------. 2 reakwon reakwon   24  5월 16 14:41 ssh-UDg27UAVaZcs
drwx------. 2 reakwon reakwon   24  5월 16 23:13 ssh-bWUCqvi956kP
drwx------. 2 reakwon reakwon   24  5월 16 22:38 ssh-g8sqmExSvLtD
        1.    2.     3.         4.         5.          6.                  7.

 

1. 파일의 종류와 권한: 이 파일이 어떤 파일인지, 소유주, 소유주 그룹, 기타 다른 유저들이 이 파일을 사용할때의 권한을 보여줍니다. 

파일의 종류는 다음과 같습니다.

    a) - : 일반 파일

    b) d : 디렉토리

    c) b : block 장치 파일

    d) c : character 장치 파일

    e) l : 심볼릭 링크 파일

    f) p : 명명된 파이프

    g) s : 소켓 파일

 

권한은 다음과 같이 지정됩니다. 각 3개씩 권한이 나누어 지는데요.

rwx(파일 소유자의 권한)r-x(파일 소유 그룹의 권한)r-x(그외 사용자에 대한 권한)

r : read , w : write , x : execute

 

위의 파일 권한을 해석해본다면 파일 소유자는 읽기,쓰기, 실행 권한을 갖고 있네요. 파일 소유자가 있는 그룹은 이파일을 읽고, 실행만 할 수 있겠네요. 다른 사용자는 읽고, 실행만 할 수 있습니다.

 

2. 하드링크수를 보여줍니다. 

3. 파일의 소유주를 보여줍니다. 디렉토리를 만들거나 파일을 만든 계정입니다. 파일의 소유주는 chown명령어를 이용해 바꿀 수 있습니다.

4. 소유그룹을 보여줍니다. 파일 소유주의 그룹을 나타냅니다. 

5. 파일의 크기를 보여줍니다.

6. 최종 수정일, 시간을 보여줍니다.

7 마지막으로 파일명을 보여줍니다.

 

 

 

i : inode번호를 보여줍니다. 디렉토리에는 파일명과 해당 파일의 inode번호가 매핑되어 있는데, ls -l과 같이 파일의 자세한 정보를 볼 수 있는 것은 inode에 파일에 대한 메타데이터가 기록이 되기 때문입니다. 어떤 것들이 있는지는 아래와 같습니다.

  • 파일 모드 : 파일의 형식과 실행 권한
  • 링크 수 : 이 아이노드에 대한 디렉터리 참조 수
  • 소유자 계정 : 파일의 소유자
  • GID : 이 파일과 관계된 그룹 소유자
  • 파일 크기 : 파일의 바이트
  • 파일 주소 : 주소 정보
  • 마지막 접근(Access) : 마지막으로 파일에 접근한 시각
  • 마지막 수정(Modified) : 마지막으로 파일을 수정한 시각
  • 아이노드 수정(Changed) : 마지막으로 아이노드를 수정한 시각

: 하위 디렉토리의 내용까지 보여줍니다. 예를 들어 현재 디렉토리가 aaa이며 파일이 bbb,ccc 그리고 그 하위 디렉토리가 ccc이며 ccc의 디렉토리에 eee,fff 파일이 있다면 디렉토리 aaa의 파일을 전부 출력해주며 하위 디렉토리인 ccc의 내용까지 전부 출력해주는 옵션입니다.

: 디렉토리인지, 어떤 타입의 파일인지를 보여줍니다.

 

반응형
블로그 이미지

REAKWON

와나진짜

,

쓰레드에 대한 더 많은 정보를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

스레드(Thread)

 

스레드는 한국어로 바꿔말하면 실이라고 합니다. 우선 개념부터 잡고 갑시다.

스레드는 어떤 프로그램에서 프로세스가 실행되는 흐름의 단위를 말합니다.

프로세스는 적어도 하나의 스레드를 갖고 있습니다. 우리가 흔히 알고 있는 main함수가 바로 그 스레드지요.

 

멀티 프로세스와 멀티 스레드는 흐름이 동시에 진행된다는 것에서 공통점을 갖습니다. 하지만 프로세스와는 다르게 메모리를 공유한다는 점이 다른데요.

아래의 그림과 같이 스레드간 공유되는 영역은 code, data, file입니다. 스택은 스레드마다 독립적으로 갖고 있다는 것을 알 수 있지요.

 

 

 

리눅스의 스레드

리눅스에서 스레드를 생성하려고 한다면 pthread.h의 헤더파일을 포함해야합니다.

 

pthread_create

스레드를 생성합니다. 함수 원형은 이렇습니다.

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg)

 

이 함수는 현재 실행되고 있는 프로세스에서 새로운 스레드를 생성하는 함수입니다.

 

1. thread : 성공적으로 함수가 호출되면 이곳에 thread ID가 저장됩니다. 이 인자로 넘어온 값을 통해서 pthread_join과 같은 함수를 사용할 수 있습니다.

2. attr : 스레드의 특성을 정의합니다. 기본적으로 NULL을 지정합니다. 만약 스레드의 속성을 지정하려고 한다면 pthread_attr_init등의 함수로 초기화해야합니다.

3. start_routine : 어떤 로직을 할지 함수 포인터를 매개변수로 받습니다. 

4. arg : start_routine에 전달될 인자를 말합니다. start_routine에서 이 인자를 변환하여 사용합니다.

성공시에 thread를 초기화함과 동시에 0을 반환하게 됩니다. 실패시에는 thread인자가 설정되지 않고 에러 값을 반환하게 되지요.

 

아래의 코드가 pthread_create를 사용한 예를 보여줍니다.

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

void* thread_routine(void *arg){  
        pthread_t tid;    
        tid=pthread_self();  

        int i=0;    
        printf("\ttid:%lx\n",tid);   
  
        while(i<10){    
                printf("\tnew thread:%d\n",i);    
                i++;        
                sleep(1);  
        } 
} 

int main(){    
        pthread_t thread;   
        pthread_create(&thread,NULL,thread_routine, NULL);   
        int i=0;  
        printf("tid:%lx\n",pthread_self());     
    
        while(i<5){     
                printf("main:%d\n",i);     
                i++;        
                sleep(1);   
        } 
}

pthread_create를 통해서 thread_routine을 실행하고 있지요. 코드를 더 간략하게 하기 위해서 오류처리는 하지 않았습니다.

 

gcc pthread.c -lpthread -o pthread의 명령을 주고나서 pthread라는 프로그램을 실행시켜보도록 하겠습니다.

 

tid:ac402740

main:0

        tid:abc11700

        new thread:0

main:1

        new thread:1

main:2

        new thread:2

main:3

        new thread:3

main:4

        new thread:4

 

메인스레드와 thread라는 스레드는 각자의 thread id를 출력하고 1초에 한번씩 i의 값을 증가시키면서 출력해주고 있습니다. thread id는 서로 다르다는 것을 알 수 있습니다.

 

자. thread_routine은 0부터 9까지 1초에 한번씩 출력해줘야하는 우리의 의도와는 다르게 main이 끝나면서 동시에 끝나게 됩니다.

우리는 thread_routine이 끝날때까지 프로그램을 멈추고 싶지 않습니다. 이 의도를 달성하기 위해서 우리는 어떻게 해결해야할까요?

 

pthread_join

우리의 목적을 달성하기 위한 함수입니다.

int pthread_join(pthread_t thread, void **retval)

pthread_join은 스레드를 생성했던 thread를 끝날때까지 기다려줍니다. 만약 thread가 이미 종료되었다면 즉시 리턴합니다.

 

1. thread : 우리가 join하려고 하는 thread를 명시해줍니다. pthread_create에서 첫번째 인자가 있었죠? 그 스레드가 join하길 원한다면 이 인자로 넘겨주면 됩니다.

2. retval : pthread_create에서 start_routine이 반환하는 반환값을 여기에 저장합니다. 

 

만약 성공적으로 호출이 되었다면 0을 반환합니다. 실패시 에러 넘버를 반환하게 되지요. 실패시에는 좀비 스레드가 되고 이 좀비 스레드는 자원을 소모하게 되어 더이상 스레드를 생성할 수 없게 된다고 하네요.

 

pthread_join을 통해서 thread_routine을 전부 실행시키도록 main 스레드에서 기다려 주도록 해볼게요.

 

int main{ 
        // 생략 //  
        while(i<5){      
                printf("main:%d\n",i);      
                i++;         
                sleep(1);   
        }     
    
        pthread_join(thread,NULL);
}

main함수 맨 아래에 pthread_join만 추가해주면 됩니다. thread_routine은 반환하는 값이 없으므로 두번째 인자는 NULL을 전달해주는 것이구요.

 

이제 결과를 한번 보도록하지요.

tid:47ee9740

main:0

        tid:476f8700

        new thread:0

main:1

        new thread:1

main:2

        new thread:2

main:3

        new thread:3

main:4

        new thread:4

        new thread:5

        new thread:6

        new thread:7

        new thread:8

        new thread:9

 

 

이제 thread_routine이 실행되고 끝나는 것을 볼 수가 있네요.

 

pthread_detach

 

때에 따라서는 스레드가 독립적으로 동작하길 원할 수도 있습니다. 단지 pthread_create 후에 pthread_join으로 기다리지 않구요. 나는 기다려주지 않으니 끝나면 알아서 끝내도록 하라는 방식입니다.

 

독립적인 동작을 하는 대신에 스레드가 끝이나면 반드시 자원을 반환시켜야합니다. pthread_create만으로 스레드를 생성하면 루틴이 끝나서도 자원이 반환되지 않습니다. 그러한 문제점을 해결해주는 함수가 바로 pthread_detach입니다. 

int pthread_detach(pthread_t thread)

thread는 우리가 detach 시킬 스레드입니다. 

성공시 0을 반환하며 실패시 오류 넘버를 반환하지요.

pthread_detach와 pthread_join을 동시에 사용할 수는 없습니다.

 

두번째 detach 방식은 바로 생성과 동시에 detach 시키는 방법입니다. pthread_create에서 두번째 인자 기억나시나요? attr에 detach정보를 주어 생성과 동시에 분리시키는 것이지요.

 int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

 

사용법은 이렇습니다.

pthread_attr_t attr;

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

pthread_create(.., &attr, ...)

pthread_attr_setdetachstate에는 PTHREAD_CREATE_DETACHED 뿐만 아니라 PTHREAD_CREATE_JOINABLE이라는 pthread_create 동시에 join 시킬 수도 있습니다.

두번째 방법이 훨씬 안전한 방법이라고 얘기해 드리고 싶군요. attr을 통해서 detach하는 방법 말이죠.

전자의 방법으로 했다가 정말 재수 없다면 pthread_detach 전에 스레드가 끝날 수 있는 상황이 발생하기 때문이지요.

리눅스의 스레드 기본 함수들과 사용법을 살펴보았습니다.

 

스레드 동기화

스레드를 사용할때는 주의해야함 점이 있습니다. 서로 동시에 실행하기 때문에 발생하는 공유자원의 잘못된 접근때문입니다. 그 때문에 공유자원을 하나의 스레드만 사용하게 하는 방법이 필요한데 이를 동기화라고 합니다. 

스레드를 통한 동기화는 아래의 포스팅을 참고하시기 바랍니다.

 

Mutex(pthread_mutex_init, pthread_mutex_lock, pthread_mutex_unlock, pthread_mutex_destroy)를 사용한 스레드 동기화 ->

 

https://reakwon.tistory.com/98

 

조건 변수의 사용은 아래의 포스팅을 참고하시기 바랍니다.

https://reakwon.tistory.com/99

 

반응형
블로그 이미지

REAKWON

와나진짜

,

시그널에 대한 더 많은 정보를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

시그널에 대해서 이야기하는 3번째 시간이 되겠네요. 지난 번에는 시그널 개념과 시그널 관련함수까지 다루어 봤습니다. 

시그널 개념과 시그널 핸들러와 시그널 관련 함수(sigfillset, sigemptyset, sigaddset, sigdelset, sigprocmask)는 지난 번 포스팅을 참고하시기 바랍니다.

 

이번에는 시그널에 대한 sigpending과 sigismember에 대해서 알아보도록 하겠습니다.

  •  sigpending
int sigpending(sigset_t *set);

 

현재 블록되어 대기중인 signal들을 set에 담게 됩니다. 만일 블록될 signal들이 {SIGINT, SIGTSTP} 라고 하고 SIGTSTP가 블록된 상태라면 set에는 {SIGTSTP}가 들어가게 되지요. 이 함수가 성공했다면 0, 실패했다면 -1을 반환합니다.

  • sigisempty
int sigismember(sigset_t *set, int signo);

 

현재 set에 signo(시그널 번호)가 포함되어 있는지 알아냅니다. 만약 set에 해당하는 시그널 번호가 존재한다면 1, 없다면 0을 반환하게 됩니다. -1을 반환할때도 있는데 이 경우는 시그널을 확인할 수 없다는 의미랍니다. 간단하죠?

 

이제 간단하게 예제를 보도록 합시다.

 

#include <stdio.h>
#include <signal.h> 
#include <unistd.h>  

int main(){
        sigset_t pendingset;
        sigset_t set;

        sigemptyset(&set);
        sigemptyset(&pendingset);

        sigaddset(&set,SIGQUIT);
        sigaddset(&set,SIGINT);
        sigaddset(&set,SIGTSTP);

        sigprocmask(SIG_BLOCK,&set,NULL);

        printf("SIGQUIT, SIGINT, SIGTSTP를 발생시켜보세요.\n");
        sleep(3);

        if(sigpending(&pendingset)==0){
                printf("\n\nBlock되어 대기중인 SIGNAL\n");
                if(sigismember(&pendingset,SIGQUIT))
                        printf("SIGQUIT\n");
                if(sigismember(&pendingset,SIGINT))
                        printf("SIGINT\n");
                if(sigismember(&pendingset,SIGTSTP))
                        printf("SIGTSTP\n");
        }

        sleep(3);

        sigprocmask(SIG_UNBLOCK,&set,NULL);

        printf("SIGQUIT OR SIGINT OR SIGTSTP 신호를 발생시켰으면 이 메
시지가 보이지 않습니다.\n");
        return 0;
}

이 프로그램은 set에 있는 시그널들이 발생하면 블록하고 만약 이 중 시그널이 발생하면 블록된 시그널의 목록을 보여주는 프로그램입니다. 우선 set에는 SIGQUIT, SIGINT, SIGTSTP라는 시그널이 들어있고 sigprocmask로 블록시키라고 명령합니다. 이 후 3초가 지난후 sigpending으로 블록되어 대기중인 signal을 pendingset에 설정합니다. 함수가 성공적으로 호출되었다(반환값 0)면 sigismember에서 어떤 시그널이 대기중인지 확인합니다.

 

확인해봅시다. gcc 소스코드 -o signal 로 컴파일하고 실행파일을 실행시켜봅시다.

# ./signal

SIGQUIT, SIGINT, SIGTSTP를 발생시켜보세요.

^C^Z

Block되어 대기중인 SIGNAL

SIGINT

SIGTSTP

저는 3초이내 Ctrl+C(SIGINT)와 Ctrl+Z(SIGTSTP)를 발생시키고 메시지를 확인해보니 SIGINT와 SIGTSTP가 대기중이라고 나옵니다. 이해되셨나요?

 

  •  sigsuspend
int sigsuspend(const sigset_t *mask);

시그널을 BLOCK시킴과 동시에 대기합니다. sigprocmask같은 경우 how를 SIG_BLOCK이나 SIG_SETMASK로 설정하면 블록하기만 할뿐 대기하지는 않는데, sigsuspend는 블록과 대기를 동시에 할 수 있는 것이죠. 성공시 0, 실패시 -1을 반환합니다.

아래의 예제가 있습니다.

 

 

#include <stdio.h> 
#include <signal.h> 
#include <unistd.h> 

int main(){    
        sigset_t set;    
   
        sigemptyset(&set);   
 
        sigaddset(&set,SIGQUIT);   
        sigaddset(&set,SIGINT); 
        sigaddset(&set,SIGTSTP);   
 
        printf("SIGQUIT, SIGINT, SIGTSTP이 5초간 BLOCK됩니다.\n");  
 
        sigprocmask(SIG_SETMASK,&set,NULL);    
   
        sleep(5);     
        printf("\n\nSIGNAL 블록이 해제되고 시그널이 발생했다면 종료됩니다.\n");        
        printf("시그널을 발생시키지 않았다면 발생시켜 종료하세요.\n");      
        sigemptyset(&set);      
        sigsuspend(&set);   
  
        return 0;
}


SIGQUIT, SIGINT, SIGTSTP를 sigprocmask의 SIG_SETMASK로 블록시키려고 하는군요. 5초 후에 set을 빈 집합으로 설정합니다. 만일 SIGQUIT, SIGINT, SIGTSTP가 5초 이내에 발생했다면 프로그램은 5초후 바로 종료하게 되고, 발생시키지 않았다면 위 3개의 시그널이 발생할때까지 대기하게 되는 것입니다.

  •  5초 내에 SIGINT 신호를 전달한 경우 5초가 지난 후 메시지 출력후 바로 종료
# ./a.out 
SIGQUIT, SIGINT, SIGTSTP이 5초간 BLOCK됩니다.
^C

SIGNAL 블록이 해제되고 시그널이 발생했다면 종료됩니다.
시그널을 발생시키지 않았다면 발생시켜 종료하세요.
  • 5초내에 3개의 신호 아무것도 를 주지 않은 경우 신호를 줄때까지 계속 대기 
# ./a.out 
SIGQUIT, SIGINT, SIGTSTP이 5초간 BLOCK됩니다.


SIGNAL 블록이 해제되고 시그널이 발생했다면 종료됩니다.
시그널을 발생시키지 않았다면 발생시켜 종료하세요.

 

이상으로 3개의 signal 관련 함수를 알아보았습니다.

 

반응형
블로그 이미지

REAKWON

와나진짜

,