본문 바로가기

Programming/Linux_Kernel

frame buffer 이야기(4) (5)

원문 : http://kelp.or.kr/korweblog/stories.php?story=02/11/11/1742716
http://kelp.or.kr/korweblog/stories.php?story=02/11/12/3096836


글쓴이 : holelee (2002년 11월 11일 오후 10:35) 읽은수: 17,354 [ 임베디드강좌/이규명 인쇄용 페이지 ]
== 시작하기에 앞서
이 글에 있는 모든 내용은 글쓴이가 가지고 있는 ATI mach 64(2MB) 그래픽 카드가 달려있고 RedHat 8.0이 설치된 PC(또는 Permedia2(8MB) 그래픽 카드가 달려 있는 RedHat 7.3이 설치된 PC)에서 제대로 동작하지만 이 글에 있는 모든 내용이 정확하다고 말씀 드릴 수 없습니다. 이 글에 있는 내용을 따라 했을 때 혹 생길 지 모르는 모든 문제에 대해서 글쓴이는 책임을 지지 않습니다. 글의 내용 중에 잘못된 내용이 있거나 질문할 것이 있으면 위쪽의 “holelee”를 누르고 메일을 보내기 바랍니다.

== 시작
저번 글에서는 다소 무식한(?) pixel 찍기를 해 보았습니다. 왜 무식한지는 나중에 따지도록 하고 전형적인 보통 파일 쓰기의 방법에 기초한 방법이었습니다. open하고 lseek으로 offset 조정하고 write로 값을 써 넣었습니다. 그런데 보통의 파일 쓰기의 경우 그냥 주르륵 써 넣기만 하지 쓸 때마다 매번 lseek으로 offset을 조정하거나 하는 것은 매우 드믄 경우가 됩니다. 이렇게 매번 lseek으로 offset을 조정하는 것은 상당히 프로그래밍을 하기가 피곤합니다. 좀더 좋은 방법이 없을까요? mmap 시스템 호출이라는 좋은 방법이 있습니다. 이번 글에서는 일반적인 mmap 시스템 호출에 대해서 알아보고 다음 글에서는 mmap을 frame buffer에 이용하는 법을 알아보도록 하겠습니다.

== mmap 시스템 호출
역사적 배경이야 잘 모르겠고, “파일 읽기 쓰기를 메모리 읽기 쓰기와 동일하게 할 수 없을까?”라는 소박한(?) 요구에 부응하는 것이 mmap 시스템 호출입니다. mmap 시스템 호출은 file descriptor로 표현(?)되는 object(C++도 아니니 객체로 번역하기도 껄끄럽고 그렇다고 컴파일할 때 나오는 object 코드를 이야기하는 것도 아닙니다. 그냥 개체)를 application의 virtual address상에 매핑(mapping)시켜주는 시스템 호출입니다. 그냥 쉽게 생각해서 파일을 열어서 mmap하면 read/write를 포인터로 할 수 있다고 알아두면 쉽습니다. 이러한 시스템 호출은 linux상에도 있고 Windows도 비슷한 일을 하는 시스템 호출이 있는 것으로 보아 많은 현대적인 OS에 존재하는 것 같습니다. 사용법을 알아보죠.
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
이것이 mmap의 man page에 나오는 mmap 시스템 호출의 prototype입니다. 우선 그냥 특별한 경우가 아니면 start는 0을 넣는다고 외우세요.(즉 일반적인 경우 크게 의미가 없는 argument입니다.) length는 메모리로 매핑할 크기가 됩니다. prot, flags는 우선 무시하고 fd는 매핑할 file descriptor입니다. 그리고 offset은 fd로 표현되는 object상의 offset입니다. 리턴 값은 어드레스구요. 제가 한마디로, 그러나 어설프게 mmap이 무엇을 해주는 가를 말해보도록 하겠습니다. object 대신 파일이라는 말을 쓰면 이해하기 쉬우니 파일을 써서 하겠습니다.
“mmap 시스템 호출은 fd로 지정된 파일에서 offset을 시작으로 length바이트만큼을 메모리에 매핑하고 그 어드레스를 리턴한다.”(사실 한글 man page에서 적당히 베껴서 편집한 말입니다.)
argument prot은 메모리 매핑이 읽기/쓰기를 어떤 형식으로 허용할지를 나타내고 flags는 이 메모리와 파일 사이의 관계(?)를 나타내는 flag값인 것 같습니다.
모든 자세한 사항은 man page를 참고로 하시기 바랍니다.

예제로 무엇을 할까 많은 고민 끝에 가장 간단하면서도 아무 쓸모가 없는 것으로 정했습니다.(마땅한 예제가 없네요.) 우선 mmap 예제를 돌리기에 앞서서 test file을 만들어 내는 프로그램 소스코드를 먼저 올립니다.
===============================
/*
* tvector.c : test vector generator for mmaptest
*
* Copyright(C) 2002 holelee
*
*/

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

int main()
{
FILE *fstream;
int i;

fstream = fopen("mmaptest.tst", "w+b");
if(fstream == NULL)
{
fprintf(stderr, "Cannot open mmaptest.tst\n");
exit(1);
}

for(i = 0; i < 1024; i++)
fwrite(&i, 4, 1, fstream);

fclose(fstream);
exit(0);
return 0;
}
===============================
그냥 C standard library에 있는 함수로 작성해 봤습니다. 내용은 간단하죠. 그냥 mmaptest.tst라는 파일 안에 0부터 1023까지 정수 값을 저장하는 프로그램입니다.(총 32비트 machine에서는 총 4096바이트에 해당하겠죠.)
이제 mmaptest.c 파일을 살펴보도록 하겠습니다.
===============================
/*
* mmaptest.c : mmap example
*
* Copyright(C) 2002 holelee
*
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

int main()
{
int fd;
int *pmmap;
int i;

fd = open("mmaptest.tst", O_RDWR);
if(fd < 0)
{
perror("open");
exit(1);
}

pmmap = (int *)mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if((unsigned)pmmap == (unsigned)-1)
{
perror("mmap");
exit(1);
}

for(i = 0; i < 1024; i++)
{
printf("%d\n", pmmap[i]); /* 읽어서 출력 */
pmmap[i] = pmmap[i]+1024; /* 값을 바꿈 */
}

munmap(pmmap, 4096);
close(fd);
exit(0);
return 0;
}
===============================
내용을 보면 동일한 파일(mmaptest.tst)를 open하고 offset 0부터 4096byte만큼을 READ/WRITE가 가능하도록 mmap합니다. mmap이 리턴한 address는 integer 포인터 pmmap에 assign하고 1024개 만큼 그 포인터에서 읽어서 출력하면서 포인터로 지정된 어드레스에 있는 값을 바꾸는 프로그램입니다. 값을 바꾸었으므로 당연히 파일에 저장된 값이 바뀔겁니다. 그 다음 munmap이라고 하여 mmap의 역함수(?)와 같은 함수를 부릅니다. munmap에 대해서 자세히 알고 싶으면 man page를 보길 바랍니다. 암튼 이제 동작을 살펴봐야 겠죠.
우선 tvector를 수행하여 파일을 만듭니다. 그 다음 mmaptest를 수행시키면 0부터 1023까지의 정수를 차례로 출력할겁니다.(눈깜짝할 사이에 지나가 버리겠지만요.) 다시 mmaptest를 수행하면 이제는 1024부터 2047까지의 값을 찍게 됩니다. 전에 수행시켰던 mmaptest가 pmmap 포인터를 이용하여 파일에 저장된 내용을 바꾸었으니까 당연한 것이죠. 계속 수행해 보면 1024씩 증가합니다.
암튼 파일 내용을 write 시스템 호출을 이용하지 않고 포인터를 이용하여 바꾸는데 성공하였습니다.
** 여기서 잠깐
mmap이 리턴하는 어드레스는 virtual address입니다. 그 address 값 자체는 큰 의미를 지니지 못합니다. CPU의 MMU가 그것을 physical address로 바꿀 때 어떤 일이 일어나겠지만요.

== file descriptor로 지정된 object
file descriptor로 지정될 수 있는 object는 상당히 많이 있습니다. 당연히 일반적인 파일이 지정될 수 있죠. 또한 socket도 file descriptor로 지정될 수 있고, IPC(Inter Process Communication)에서 사용하는 pipe, fifo 등도 역시 file descriptor로 지정될 수 있습니다. 또한 device special file(mknod로 만들어진)을 open하면 device도 지정될 수 있겠죠. 사실 file descriptor로 지정된 모든 것이 mmap을 지원하는 것은 아닙니다. 예를 들어 serial port device와 같은 경우는 모든 데이터가 순서대로 읽어지고 써지는 구조를 가지고 있는데 이런 경우에는 lseek도 의미가 없고 mmap도 되지 않을 겁니다. Serial device를 mmap하여 접근하는 것 자체가 우스운 일이죠.(혹 가능하면 글쓴이만 우스워지므로 가능하다는 사실을 알고 있으면 조용히 메일을 보내줄 것.) mmap을 지원하지 않은 object라면 mmap 시스템 호출에 -1를 리턴하고 errno를 적당한 값으로 채워줄 겁니다.

== 왜 mmap 이야기를 꺼냈는가?
Frame buffer 이야기를 하다가 왜 일반적인 mmap 이야기가 나왔을까요? 이쯤 되면 짐작이 되겠지만 frame buffer device도 file descriptor로 지정될 수 있고 일반적으로 mmap을 지원합니다. 따라서 lseek/write 시스템 호출에서 벗어나서 그냥 포인터로 접근을 해서 화면에 출력될 pixel 값을 바꾼다던지 화면에 출력된 pixel 값을 읽어 올 수 있습니다. lseek/write 시스템 호출을 사용하는 것은 나중에 살펴보겠지만 엄청 무식한 방법입니다. 단, 모든 frame buffer가 mmap을 지원하지는 않는 것 같습니다. 커널 소스의 Documentation/fb/tgafb.txt를 보면 DECChip 21030 칩 기반 그래픽 카드는 mmap을 지원하지 않는 것으로 나와 있습니다. 아마도 시스템의 구조상 버퍼 메모리를 CPU의 virtual address로 매핑할 수 없기 때문일 것으로 생각됩니다. 하지만 일반적인 frame buffer라면 대부분 mmap을 지원하고 mmap을 해서 사용하는 것이 일반적입니다. StrongARM의 LCD도 당연히 mmap을 지원할 겁니다. 왜냐하면 시스템 메모리의 일부를 LCD 버퍼로 이용하므로 virtual address로의 매핑이 쉽게 가능하기 때문이죠.

== 마치며
이번 글에서는 일반적인 mmap 시스템 호출에 대해서 알아보았습니다. 다음 글에서는 좀 유식한(?) mmap을 이용한 pixel 찍는 방법에 대해서 알아보겠습니다. 그리고 이번 글부터는 소스코드에 nl을 이용하여 줄 숫자를 적는다거나 하는 작업을 하지 않을 겁니다. 어짜피 KELP에서 잘 보이지 않네요. 잘 알아서 보길 바랍니다.



== 시작하기에 앞서
이 글에 있는 모든 내용은 글쓴이가 가지고 있는 ATI mach 64(2MB) 그래픽 카드가 달려있고 RedHat 8.0이 설치된 PC(또는 Permedia2(8MB) 그래픽 카드가 달려 있는 RedHat 7.3이 설치된 PC)에서 제대로 동작하지만 이 글에 있는 모든 내용이 정확하다고 말씀 드릴 수 없습니다. 이 글에 있는 내용을 따라 했을 때 혹 생길 지 모르는 모든 문제에 대해서 글쓴이는 책임을 지지 않습니다. 글의 내용 중에 잘못된 내용이 있거나 질문할 것이 있으면 위쪽의 “holelee”를 누르고 메일을 보내기 바랍니다.

== 시작
예고한대로 mmap을 이용한 무식하지 않은(?) pixel 찍는 법을 알아 볼 차례입니다. 방법은 아주 간단합니다. frame buffer device를 open하고 open이 리턴한 file descriptor를 이용하여 mmap을 합니다. 당연히 offset은 0이고 length는 화면의 해상도와 bpp를 통해 계산된 byte수 만큼이죠. mmap이 리턴한 address와 pixel을 찍기를 원하는 좌표를 이용하여 새로운 address를 계산하고 그 address에 값을 쓰면 pixel이 찍히게 되고 그 address에서 값을 읽으면 화면상에 있는 pixel 값을 읽을 수 있는 겁니다. 자 준비되었으면 소스를 보도록 하죠.

== 다시 나온 pixel 찍기 소스
===============================
/*
* fbpixel2.c : Frame buffer draw pixel example(using mmap)
*
* Copyright(C) 2002 holelee
*
*/

#include <stdio.h>
#include <stdlib.h> /* for exit */
#include <unistd.h> /* for open/close .. */
#include <fcntl.h> /* for O_RDWR */
#include <sys/ioctl.h> /* for ioctl */
#include <sys/mman.h> /* for mmap */
#include <linux/fb.h> /* for fb_var_screeninfo, FBIOGET_VSCREENINFO */

#define FBDEVFILE "/dev/fb"

typedef unsigned char ubyte;

unsigned short makepixel(ubyte r, ubyte g, ubyte b)
{
return (unsigned short)(((r>>3)<<11)|((g>>2)<<5)|(b>>3));
}

int main()
{
int fbfd;
int ret;
struct fb_var_screeninfo fbvar;
unsigned short pixel;
int offset;
unsigned short *pfbdata;

fbfd = open(FBDEVFILE, O_RDWR);
if(fbfd < 0)
{
perror("fbdev open");
exit(1);
}

ret = ioctl(fbfd, FBIOGET_VSCREENINFO, &fbvar);
if(ret < 0)
{
perror("fbdev ioctl");
exit(1);
}

if(fbvar.bits_per_pixel != 16)
{
fprintf(stderr, "bpp is not 16\n");
exit(1);
}

pfbdata = (unsigned short *)
mmap(0, fbvar.xres*fbvar.yres*16/8,
PROT_READ|PROT_WRITE, MAP_SHARED, fbfd, 0);

if((unsigned)pfbdata == (unsigned)-1)
{
perror("fbdev mmap");
exit(1);
}

/* red pixel @ (0,0) */
pixel = makepixel(255,0,0); /* red pixel */
*pfbdata = pixel;

/* green pixel @ (100,50) */
offset = 50*fbvar.xres+100;
pixel = makepixel(0,255,0); /* green pixel */
*(pfbdata+offset) = pixel; /* draw pixel */


/* blue pixel @ (50,100) */
offset = 100*fbvar.xres+50;
pixel = makepixel(0,0,255); /* blue pixel */
*(pfbdata+offset) = pixel; /* draw pixel */

/* white pixel @ (100,100) */
offset = 100*fbvar.xres+100;
pixel = makepixel(255,255,255); /* white pixel */
*(pfbdata+offset) = pixel; /* draw pixel */

munmap(pfbdata, fbvar.xres*fbvar.yres*16/8);
close(fbfd);
exit(0);
return 0;
}
===============================
먼저 소개한 fbpixel.c처럼 (0,0)에 빨간 pixel, (100, 50)에 녹색 pixel, (50, 100)에 파란 pixel, (100, 100)에 하얀 pixel을 순서대로 찍는 간단한 프로그램입니다. fbvar.xres*fbvar.yres*(16/8)만큼의 바이트만큼이 mmap으로 virtual address에 매핑되고 있습니다.(당연히 mmap을 지원하지 않는 특이한 frame buffer에서는 에러를 내고 죽게 됩니다.) 그 address로부터 픽셀이 저장된(될) address라고 보면 됩니다. 픽셀이 저장된 순서는 (0,0)부터 시작하여 (1,0), (2,0)의 순서로 (xres-1, 0)까지 진행되고 그 다음 address는 (0, 1)이 되고 다시 (1,1), (2,1),… 이런 식으로 진행됩니다. 쉽게 이해할 수 있겠죠. lseek 때의 경우와 똑같습니다. 당연히 픽셀 offset은 바이트 단위로는 먼저 소개한 fbpixel.c와 같지만 unsigned short 포인터 pfbdata와 더하므로 offset을 바이트 단위로 계산하지 않고 2byte(sizeof(unsigned short))단위로 하고 있습니다. 따라서 더 이상 (18/6)과 같은 것은 offset 계산에 필요가 없습니다. 그럼 좌표 (xpos, ypos)에 pixel을 찍는 함수를 한번 작성해 보죠.
===============================
1: void put_pixel(fb_var_screeninfo *fbvar, unsigned short *pfbdata, int xpos, int ypos, unsigned short pixel)
2: {
3: int offset = ypos*fbvar->xres+xpos;
4: pfbdata[offset] = pixel;
5: }
===============================
pfbdata에 mmap이 리턴한 address를 주면 제대로 동작할 것으로 기대됩니다.


== address에 값을 쓰면 어떤 일이 벌어지나?
mmap이 리턴한 address는 virtual address로 실제 물리적인 것과는 전혀 관계가 없음을 저번 글에서 언급했습니다. 그럼 위의 fbpixel2.c 소스 코드에서 *(fbdata+offset) = pixel이라는 수식(expression)이 수행되면 실제 PC에서는 어떤 일이 벌어질까요? 알고 있다시피 실제 화면에 나올 픽셀이 저장된 버퍼는 PC에서는 그래픽카드에 있는 RAM에 존재합니다. 그럼 C 언어의 수식하나로 그 그래픽 카드에 있는 RAM 값이 update되는 셈인데 어떤 일이 벌어지는 지 궁금하지 않나요? 제가 어떤 일이 벌어지는지 구성해 보도록 하겠습니다.
우선 CPU에서 저 수식이 수행되면 address 계산(fbdata+offset)을 하게 됩니다. 그 address에 pixel 값을 쓰는 instruction 수행되겠죠.(보통 RISC는 그런 instruction을 store라고 부르고 x86에서는 mov입니다.) 그럼 address를 CPU의 MMU가 살펴보고 physical address로 바꾸어서 CPU의 버스에 그 physical address에 pixel이라는 값을 쓰겠다고 신호를 보냅니다. 그럼 CPU 버스에 물려있는 North bridge라는 녀석이 그 address를 살펴보고 시스템에 설치된 DRAM의 어드레스가 아님을 판단하고 PCI 버스(혹은 AGP포트)에 address와 pixel 데이터로 다시 신호를 보내게 됩니다.(이 때 address translation이 다시 일어날 수도 있습니다. PCI버스와 CPU버스가 addressing이 다르다면.) 그럼 PCI 버스(혹은 AGP포트)에 달려있는 그래픽 카드가 그 address를 보고 자기 자신에 관한 일임을 알아채고 그에 상응하는 일을 해주게 됩니다. ATI mach 64 CT처럼 PCI 버스에서 동작하는 그래픽카드를 가정하고 환상적인 그림솜씨(?)를 뽐내보면 다음과 같은 그림으로 시스템을 나타내 볼 수 있습니다.

CPU(MMU)
|/
|/ <CPU버스>
|/
North Bridge --- DRAM
|/
|/ <PCI버스>
|/
그래픽카드

당연히 버스에는 latency가 존재하므로 DRAM에 접근하는 것보다는 훨씬 오래 걸립니다. 또한 Cache에 저장되면 문제가 일어나게 되므로(Cache만 update되고 화면은 바뀌지 않는 식으로) Cache를 이용할 수도 없어서 latency가 상당하죠.

(실제로 요즘 최신 PC 시스템에서는 North Bridge에 직접 PCI버스가 물려있는 것이 아니고 North Bridge에는 Point-to-Point 버스로 South Bridge가 물려있고 South Bridge에 PCI 버스가 물려 있는 것으로 알고 있습니다. 그러한 구조상의 변화 때문에 Intel에서는 Bridge라는 말을 더 이상 사용하지 않고 HUB라는 말을 쓴다고 하는군요.)


== 마치며
이번 글에서는 mmap을 이용한 깔끔한 pixel 찍기에 대해서 살펴봤습니다. 뭐 별로 어려울 것은 없을 것으로 생각됩니다. 그럼 다음 글에서는 왜 그 전에 lseek/write를 이용한 pixel 찍기가 “다소 무식한(?)” 방법이었는지 생각해 보는 시간을 갖도록 하겠습니다.


'Programming > Linux_Kernel' 카테고리의 다른 글

frame buffer 이야기(8)(9)  (0) 2009.06.04
frame buffer 이야기(6) (7)  (0) 2009.06.04
frame buffer 이야기 (3)  (0) 2009.06.04
frame buffer 이야기 (2)  (0) 2009.06.04
frame buffer 이야기 (1)  (2) 2009.06.04