본문 바로가기

IT/Linux(Unix)

리눅스 프로그래밍 - 커널 소스 분석하기 ( ftp 서버 구현, 쓰래드 다루기 )

반응형

0. 서론

 

쓰레드는 전역변수를 공유한다. 고로 전역변수가 있는 함수를 통해 버퍼링을 하면 안된다. -> 멀티 프로세스를 활용.

서버는 명령어를 분석해야 하기 때문에 readline을 사용-> \n 문자열을 받아야 하기 때문. (line 단위 read : readline)

 

length 단위 read : readn ( 커널은 자신이 정해둔 기본 단위 만큼  전송하기 때문에(헤더에 있는 값이 기준인 것 처럼 보임-확인필요), 정확한 길이만큼 데이터를 받고 사용하려면 길이만큼 작동하는 함수 - readn, writen이 필요 )

클라이언트는 데이터를 받아야 하기 때문에 readn을 사용

 

1. ftp server 구현

 

1. ls\n -> readline()

2. readn()

 

[ftp_server]

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 4096

static ssize_t
my_read(int fd, char *ptr)
{
	static int	read_cnt = 0;
	static char	*read_ptr;
	static char	read_buf[MAXLINE];

	if (read_cnt <= 0) {
again:
		if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
			if (errno == EINTR)
				goto again;
			return(-1);
		} else if (read_cnt == 0)
			return(0);
		read_ptr = read_buf;
	}

	read_cnt--;
	*ptr = *read_ptr++;
	return(1);
}

ssize_t readline(int fd, void *vptr, size_t maxlen)
{
	int		n, rc;
	char	c, *ptr;

	ptr = vptr;
	for (n = 1; n < maxlen; n++) {
		if ( (rc = my_read(fd, &c)) == 1) {
			*ptr++ = c;
			if (c == '\n')
				break;	/* newline is stored, like fgets() */
		} else if (rc == 0) {
			if (n == 1)
				return(0);	/* EOF, no data read */
			else
				break;		/* EOF, some data was read */
		} else
			return(-1);		/* error, errno set by read() */
	}

	*ptr = 0;	/* null terminate like fgets() */
	return(n);
}
/* end readline */

void my_sig(int signo )
{
	printf("signo=%d\n", signo );
	while( waitpid(-1, 0, WNOHANG) > 0 )
		;
}

int main()
{
	int sd, new_sd, ret;
	int optval, optlen, addrlen;
	struct sockaddr_in s_addr, c_addr;
	char buff[1024];

	signal( SIGCHLD, my_sig );

	sd = socket( AF_INET,  SOCK_STREAM, 0 );

	printf("sd=%d\n", sd );
	if( sd < 0 ) 
		printf("%s\n", strerror(errno) );

	// memset( &s_addr, 0, sizeof(s_addr));
	s_addr.sin_family        = AF_INET;
	s_addr.sin_port          = htons(7777);
	s_addr.sin_addr.s_addr   = htonl(INADDR_ANY);
	bzero( &s_addr.sin_zero, 8 );

	optval = 1;
	setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(int));

	ret = bind( sd, (struct sockaddr*)&s_addr, sizeof(s_addr) );
	if( ret < 0 )
	{
		perror("bind");
		exit(0);
	}

	listen( sd, 10 );
	addrlen = sizeof(c_addr);
	while(1)
	{
		new_sd = accept( sd, 0, 0 );
		// ...
		printf("new_sd=%d\n", new_sd );

		getpeername( new_sd, (struct sockaddr*)&c_addr, &addrlen );
		printf("새로운 클라이언트 접속 : ip=%s, port=%u\n", 
				inet_ntoa(c_addr.sin_addr), ntohs( c_addr.sin_port ) );

		if( fork()==0 )
		{
			int fd, f_size;
			char f_name[100];
			close(sd);
			while( 1 )
			{
				ret = readline( new_sd, f_name, sizeof f_name );
				f_name[ret-1] = 0;  // "a.c\n\0" =>"a.c\0" gets();
				fd = open( f_name, O_RDONLY); 
				f_size = lseek( fd, 0, SEEK_END );
				lseek( fd, 0, SEEK_SET );
				write(new_sd, &f_size, sizeof f_size );
				while( ret = read( fd, buff, sizeof buff ) )
					write( new_sd, buff, ret );

				ret = readline( new_sd, buff, sizeof buff );
				if( strncmp( buff, "quit\n", ret ) == 0 )
					break;
			}
			close(new_sd);
			exit(0);
		}
		close(new_sd);
	}
	close(sd);
	return 0; 
}

 

[ftp_client]

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>

ssize_t						/* Read "n" bytes from a descriptor. */
readn(int fd, void *vptr, size_t n)
{
	size_t	nleft;
	ssize_t	nread;
	char	*ptr;

	ptr = vptr;
	nleft = n;
	while (nleft > 0) {
		if ( (nread = read(fd, ptr, nleft)) < 0) {
			if (errno == EINTR)
				nread = 0;		/* and call read() again */
			else
				return(-1);
		} else if (nread == 0)
			break;				/* EOF */

		nleft -= nread;
		ptr   += nread;
	}
	return(n - nleft);		/* return >= 0 */
}

int main()
{
	int sd, fd, ret, f_size=-1, len;
	struct sockaddr_in s_addr;
	char buff[10], f_name[100];
	sd = socket( AF_INET,  SOCK_STREAM, 0 );

	printf("sd=%d\n", sd );
	if( sd < 0 ) 
		printf("%s\n", strerror(errno) );

	// memset( &s_addr, 0, sizeof(s_addr));
	s_addr.sin_family        = AF_INET;
	s_addr.sin_port          = htons(7777);
	s_addr.sin_addr.s_addr   = inet_addr("192.168.56.102");
	bzero( &s_addr.sin_zero, 8 );

	connect( sd, (struct sockaddr*)&s_addr, sizeof(s_addr) );

	while(1)
	{
		ret = read( 0, f_name, sizeof f_name );

		write( sd, f_name, ret );

		f_name[ret-1] = 0;
		fd = open( f_name, O_WRONLY | O_CREAT | O_TRUNC, 0666 );

		readn( sd, &f_size, sizeof f_size );

		while( f_size>0 )
		{
			len = (sizeof(buff) < f_size ) ? sizeof(buff) : f_size ;
			ret = readn( sd, buff, len );
			write( fd, buff, ret );
			f_size -= ret;
		}
		close(fd);

		ret = read( sd, buff, sizeof buff );
		if( ret == 0 )
			break;
	}

	close(sd);
	return 0; 
}

 

 

2 thread ( 쓰래드 )

 

* pthread_create : 쓰래드 생성 함수 [서드파티 라이브러리]

 

커널은 프로세스와 쓰래드를 구분하지 않는다.(동일한 스케줄링 구조를 갖게 된다. 둘다 그저 task) 쓰래드 또한 좀비가 될 수 있음. 이를 방지하는 함수가 pthread_join. 나눠줬던 쓰래드를 합쳐주는(?) 역할을 한다. 쓰래드는 pid로 부모와 자식을 구분 할 수 없고, 주어진 함수로 확인 된다.(인자)

-> 이렇게 보면 프로세스와 쓰래드는 동일(둘가 task)

 

[프로세스와 쓰래드의 차이점은?]

* 쓰래드 : 전역변수를 함께 사용

부모와 자식이 동일한 가상메모리(mm)를 보게 됨.

do_fork에서 주소만 복사

 

* 프로세스 : 서로 다른 전역변수를 사용

자식이 부모의 데이터를 복사해서 그곳을 바라봄.

새로운 가상메모리 공간 주소를 mm*이 갖고 있게 됨.

do_fork에서 메모리 자체를 복사.

 

* 쓰레드와 프로세스의 가상메모리 할당 방식 추가 내용: text, data, heap, tack 영역 *

text 영역 : 프로세스와 쓰래드 둘다 부모 자식이 동일한 곳을 본다.

data영역 : 프로세스도 처음에는 자식이 부모의 data를 보고 있다가, 자식 프로세스가 새로운 곳에 data를 만듬. ( 효율을 위해 잠시 copy를 늦춤.)

 

[쓰레드의 자원관리]

 

한 쓰레드에 주어지는 시간은 20ms정도. A에서 제어권을 갖다가 변수에 대한 처리를 끝내지 못하고 B로 옮겨질 경우 변수에 이상한 값이 들어 갈 수 있다. 각 전역변수에 대한 값의 locking이 필요하게 된다.

이를 해결하기 위한 방법이 필요. Locking이 필요. -> 어셈블리를 동원

- 방법

1. mutex사용

2. semaphore 사용

 

자세한 내용은 아래 블로그들을 참고

 

https://mangkyu.tistory.com/104

 

[OS] 세마포어(Semaphore) vs 뮤텍스(Mutex) 차이

아래의 내용은 이 글의 내용을 번역, 의역 및 정리한 것입니다. 1. 세마포어(Semaphore)와 뮤텍스(Mutex) 세마포어와 뮤텍스는 모두 동기화를 이용되는 도구이지만 차이가 있다. 자세한 내용은 아래와

mangkyu.tistory.com

- 원론적인 이야기

 

https://heeonii.tistory.com/14

 

[운영체제] Mutex 뮤텍스와 Semaphore 세마포어의 차이

프로세스 간 메시지를 전송하거나, 공유메모리를 통해 공유된 자원에 여러 개의 프로세스가 동시에 접근하면 Critical Section(여러 프로세스가 데이터를 공유하며 수행될 때, 각 프로세스에서 공유

heeonii.tistory.com

- 예제가 좀 더 쉬움

 

소스는 안드로이드 오픈소스를 확인해 볼 것.

결론 => 공유하는 변수는 lock 변수(mutex, semaphore)로 묶어야 한다.

 

3. tls를 활용하는 방식

 

각 쓰레드 마다 tls라는 내부 공간을 주어 활용하는 방식. 동적 메모리 할당.

 

* 문제점

동적 메모리임으로 해제의 문제가 존재한다.

해제 함수는 쓰래드가 소멸 될 때 호출되어야 한다.

아래 예시 소스로 보면 해제 함수는 readline_destructor(void *ptr)  이다. 인자인 ptr은 해당 동적메모리 위치를 주게 됨.

 

 

예시 - unpv12e의 소스(/unpv12e/threads/readline.c)*

/* include readline1 */
#include    "unpthread.h"

static pthread_key_t    rl_key;
static pthread_once_t   rl_once = PTHREAD_ONCE_INIT;

static void
readline_destructor(void *ptr)
{
    free(ptr);
}

static void
readline_once(void)
{
    Pthread_key_create(&rl_key, readline_destructor);
}

typedef struct {
  int    rl_cnt;            /* initialize to 0 */
  char  *rl_bufptr;         /* initialize to rl_buf */
  char   rl_buf[MAXLINE];
} Rline;
/* end readline1 */

/* include readline2 */
static ssize_t
my_read(Rline *tsd, int fd, char *ptr)
{
    if (tsd->rl_cnt <= 0) {
again:
        if ( (tsd->rl_cnt = read(fd, tsd->rl_buf, MAXLINE)) < 0) {
            if (errno == EINTR)
                goto again;
            return(-1);
        } else if (tsd->rl_cnt == 0)
            return(0);
        tsd->rl_bufptr = tsd->rl_buf;
    }

    tsd->rl_cnt--;
    *ptr = *tsd->rl_bufptr++;
    return(1);
}


ssize_t
readline(int fd, void *vptr, size_t maxlen)
{
    int     n, rc;
    char    c, *ptr;
    Rline   *tsd;

    Pthread_once(&rl_once, readline_once);
    if ( (tsd = pthread_getspecific(rl_key)) == NULL) {
        tsd = Calloc(1, sizeof(Rline));     /* init to 0 */
        Pthread_setspecific(rl_key, tsd);
    }

    ptr = vptr;
    for (n = 1; n < maxlen; n++) {
        if ( (rc = my_read(tsd, fd, &c)) == 1) {
            *ptr++ = c;
            if (c == '\n')
                break;
        } else if (rc == 0) {
            if (n == 1)
                return(0);  /* EOF, no data read */
            else
                break;      /* EOF, some data was read */
        } else
            return(-1);     /* error, errno set by read() */
    }

    *ptr = 0;
    return(n);
}
/* end readline2 */

ssize_t
Readline(int fd, void *ptr, size_t maxlen)
{
    ssize_t     n;

    if ( (n = readline(fd, ptr, maxlen)) < 0)
        err_sys("readline error");
    return(n);
}

 

 

참고

* 가상메모리 : 순서대로 메모리가 자리한다.

* 물리메모리 : 랜덤으로 메모리 순서가 잡힌다.

* $ mkfifo mufifo -> mkfifo : fifo 만드는 쉘 명령어

* mm은 task_struct의 멤버변수 , 가상메모리의 위치를 갖고 있는다(간단, 사실 가상메모리의 위치를 포함한 어떤 struct를 갖고 있음).

* 가상메모리에 대한 내용참고 : https://hyunalee.tistory.com/108

 

가상메모리 영역

이 페이지는 C언어를 기준으로 가상메모리 영역에 대해 설명하고 있다. 메모리 데이터와 명령어를 저장하는 곳 컴퓨터가 작업을 할 때 그 작업을 어떻게 하는지 참고하기위한 자료가 메모리에

hyunalee.tistory.com

* 전역변수 영역은 bss로 0초기화

* 참고하면 좋은 소스(unpv12e) - wget http://kohala.com/start/unpv12e/unpv12e.tar.gz 

반응형