[{"content":"BO를 사용자 공간에 mmap해서 CPU로 직접 읽고 쓸 수 있게 했다.\n드라이버 코드는 한 줄도 없다. accel 장치 파일의 mmap은 이미 DRM 공통 함수(drm_gem_mmap)로 이어져 있고, BO는 shmem 헬퍼의 mmap을 쓴다.\nmmap에는 12편의 mmap 오프셋을 넘긴다. 커널은 이 오프셋으로 BO를 찾고, 이 fd에 권한이 있는지 본 뒤 매핑한다.\n새 BO는 0으로 차 있었고, 쓴 값은 munmap했다가 다시 매핑해도 그대로였다.\nBO보다 긴 매핑, BO 중간부터의 매핑, MAP_PRIVATE는 EINVAL이다. 오프셋 0과 닫은 BO의 오프셋도 찾을 BO가 없어서 EINVAL이다.\n다른 fd의 오프셋은 EACCES다. 오프셋 공간은 장치에 하나라 BO는 찾지만, 그 fd에는 권한이 없다.\n11편에서 BO를 만들 때는 페이지를 잡지 않는다고 했는데, 그 페이지를 잡는 게 mmap이다.\nmmap하는 순간 페이지 목록표를 만들고, BO의 페이지를 전부 메모리에 올려 고정한다. 목록표는 페이지마다 8바이트라 BO 크기의 1/512다.\n그래서 11편의 가장 큰 BO(0xFFFFF00000바이트)를 작은 프로그램으로 mmap해 봤다.\n~ # bigmmap size 0xfffff00000 offset 0x100000000 [ 14.547686] bigmmap: vmalloc error: size 2147481600, exceeds total pages, mode:0xcc0(GFP_KERNEL), nodemask=(null),cpuset=/,mems_allowed=0 [ 14.549670] CPU: 0 UID: 0 PID: 81 Comm: bigmmap Tainted: G O 7.2.0 #1 PREEMPT(lazy) [ 14.549676] Tainted: [O]=OOT_MODULE [ 14.549677] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.17.0-0-gb52ca86e094d-prebuilt.qemu.org 04/01/2014 [ 14.549679] Call Trace: 호출 스택과 메모리 상태가 90줄 가까이 이어진 뒤에 mmap이 실패했다.\nmmap failed: Cannot allocate memory 목록표가 약 2 GiB라 VM의 전체 메모리(330 MB)보다 컸고, vmalloc이 경고를 찍고 거절했다.\n호출 스택을 뿜게 두는 대신, BO를 만들 때 상한을 검사해서 넘으면 에러 코드를 돌려주게 했다.\nivpu는 BO 핸들을 만들 때 장치 주소를 정해진 범위에서 잡아 주고, 범위 크기는 하드웨어 세대마다 고정이다. 37XX 다음 세대는 256 GiB다.\n범위보다 큰 BO는 받을 주소가 없어서 만들 때 막힌다.\nvnpu도 장치 주소 공간 크기를 고정하고, 그 크기를 BO 크기의 상한으로 두기로 했다.\n후보는 128 GiB와 64 GiB였고, 둘 다 mmap해 봤다.\n~ # bigmmap 0x2000000000 size 0x2000000000 offset 0x100000000 mmap failed: Cannot allocate memory ~ # bigmmap 0x1000000000 size 0x1000000000 offset 0x100000000 mmap failed: Cannot allocate memory 둘 다 경고 없이 ENOMEM으로 끝났다.\n차이는 여유다. 128 GiB의 목록표 256 MiB는 그때 빈 메모리(286 MB)와 거의 같고, 64 GiB의 128 MiB는 그 절반이다. 그래서 64 GiB로 정했다.\nmmap이 성공하려면 결국 BO의 페이지 전부가 그 순간 빈 메모리에 들어가야 한다.\n상한은 그걸 보장하지 않고, 목록표가 메모리에 여유 있게 들어가는 쪽으로 상황을 이끌 뿐이다.\n상한을 넘는 BO는 EINVAL로 거절한다. ivpu는 주소를 못 받으면 ENOSPC가 나오는데, 주소 공간 전체보다 큰 BO는 언제 요청해도 들어갈 수 없어서 잘못된 인자로 봤다.\n64 GiB보다 1바이트 큰 BO는 EINVAL, 64 GiB BO는 만들어진다.\n# PASSED: 26 / 26 tests passed. # Totals: pass:26 fail:0 xfail:0 xpass:0 skip:0 error:0 Repo: github.com/donggeunyoo/vnpu\n","permalink":"https://donggeunyoo.github.io/posts/vnpu-13/","summary":"\u003cp\u003eBO를 사용자 공간에 mmap해서 CPU로 직접 읽고 쓸 수 있게 했다.\u003cbr\u003e\n드라이버 코드는 한 줄도 없다. accel 장치 파일의 mmap은 이미 DRM 공통 함수(drm_gem_mmap)로 이어져 있고, BO는 shmem 헬퍼의 mmap을 쓴다.\u003cbr\u003e\nmmap에는 12편의 mmap 오프셋을 넘긴다. 커널은 이 오프셋으로 BO를 찾고, 이 fd에 권한이 있는지 본 뒤 매핑한다.\u003c/p\u003e\n\u003cp\u003e새 BO는 0으로 차 있었고, 쓴 값은 munmap했다가 다시 매핑해도 그대로였다.\u003cbr\u003e\nBO보다 긴 매핑, BO 중간부터의 매핑, MAP_PRIVATE는 EINVAL이다. 오프셋 0과 닫은 BO의 오프셋도 찾을 BO가 없어서 EINVAL이다.\u003cbr\u003e\n다른 fd의 오프셋은 EACCES다. 오프셋 공간은 장치에 하나라 BO는 찾지만, 그 fd에는 권한이 없다.\u003c/p\u003e","title":"vnpu 개발기 #13 - 버퍼 객체 mmap과 크기 상한"},{"content":"핸들을 주면 BO의 크기와 mmap 오프셋을 돌려주는 BO_INFO ioctl을 넣었다.\n11편에서는 1바이트로 만든 BO가 정말 4096바이트가 됐는지 볼 방법이 없었는데, 이제 크기를 읽어 확인할 수 있다.\n오프셋은 다음에 BO를 mmap할 때 쓴다.\nstruct drm_vnpu_bo_info { __u32 handle; __u32 pad; __u64 size; __u64 mmap_offset; }; pad는 size를 8바이트 경계에 두려고 채운 칸이고, 0이 아니면 거절한다.\nivpu는 BO의 flags와 장치 쪽 주소도 돌려주는데, vnpu에는 정의한 flags도 장치 쪽 주소도 아직 없어서 뺐다.\n핸들러는 이 fd의 핸들 표에서 BO를 찾아 크기와 오프셋을 채운다. 못 찾으면 ENOENT다.\n크기는 올림이 바뀌는 경계와 가장 큰 값을 봤다.\n1과 4096은 4096, 4097은 8192가 나왔다.\n가장 큰 0xFFFFF00000도 그대로 나왔다. 4 GiB가 넘는 값도 잘리지 않는다.\n오프셋은 0이 아니고, 두 BO의 구간이 겹치지 않았다.\n없는 핸들은 세 가지를 넣었다. 유효하지 않은 핸들 값 0, 닫은 핸들, 다른 fd에서 만든 핸들이다. 셋 다 ENOENT다.\npad에 1을 넣으면 EINVAL이다.\n# PASSED: 19 / 19 tests passed. # Totals: pass:19 fail:0 xfail:0 xpass:0 skip:0 error:0 Repo: github.com/donggeunyoo/vnpu\n","permalink":"https://donggeunyoo.github.io/posts/vnpu-12/","summary":"\u003cp\u003e핸들을 주면 BO의 크기와 mmap 오프셋을 돌려주는 BO_INFO ioctl을 넣었다.\u003cbr\u003e\n11편에서는 1바이트로 만든 BO가 정말 4096바이트가 됐는지 볼 방법이 없었는데, 이제 크기를 읽어 확인할 수 있다.\u003cbr\u003e\n오프셋은 다음에 BO를 mmap할 때 쓴다.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"n\"\u003edrm_vnpu_bo_info\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003e__u32\u003c/span\u003e \u003cspan class=\"n\"\u003ehandle\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003e__u32\u003c/span\u003e \u003cspan class=\"n\"\u003epad\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003e__u64\u003c/span\u003e \u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003e__u64\u003c/span\u003e \u003cspan class=\"n\"\u003emmap_offset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003epad는 size를 8바이트 경계에 두려고 채운 칸이고, 0이 아니면 거절한다.\u003cbr\u003e\nivpu는 BO의 flags와 장치 쪽 주소도 돌려주는데, vnpu에는 정의한 flags도 장치 쪽 주소도 아직 없어서 뺐다.\u003c/p\u003e\n\u003cp\u003e핸들러는 이 fd의 핸들 표에서 BO를 찾아 크기와 오프셋을 채운다. 못 찾으면 ENOENT다.\u003c/p\u003e","title":"vnpu 개발기 #12 - 버퍼 객체 정보 읽기"},{"content":"사용자 프로그램이 장치에 넘길 메모리를 만들 수 있게 했다.\n이 메모리 덩어리를 버퍼 객체(BO, buffer object)라고 한다. ivpu에서도 작업을 제출할 때 필요한 BO들을 핸들 배열로 넘긴다.\nBO는 DRM의 메모리 관리자인 GEM(Graphics Execution Manager)이 관리한다.\nGEM은 BO 안에 뭐가 들었는지는 모르고, 사용자 공간에는 BO를 핸들이라는 번호로 보여 준다.\n핸들 표는 장치 파일을 open할 때마다 따로 생긴다.\nBO 뒤의 메모리는 shmem 헬퍼로 만든다.\nshmem은 커널 안의 공유 메모리 파일 시스템이고, 사용자 공간에는 tmpfs로 보인다. 헬퍼는 BO마다 그 크기의 shmem 파일을 하나 만들어 둔다.\nivpu의 BO도 이 헬퍼 위에 있다.\nuapi 구조체는 크기와 flags를 넣고 핸들을 받는다.\nstruct drm_vnpu_bo_create { __u64 size; __u32 handle; __u32 flags; }; ivpu 구조체에 있는 장치 쪽 주소는 아직 쓸 데가 없어서 뺐다.\nflags는 아직 정의한 비트가 없어서 0이 아니면 거절한다.\n드라이버는 DRIVER_GEM을 켜야 했다. 이게 켜져 있어야 open 때 fd마다 핸들 표가 생긴다.\n핸들러는 크기를 4 KiB 배수로 올린 값이 0이면 거절하고, shmem BO를 만들어 핸들을 붙인다.\n올린 값이 0이 되는 건 0을 넣었을 때와, 2^64 근처 값이 올림하다 넘칠 때다.\n핸들이 진짜 BO를 가리키는지는 DRM 공통 ioctl인 GEM_CLOSE로 봤다.\n만든 핸들을 닫으면 성공하고, 같은 번호를 한 번 더 닫으면 EINVAL이 온다.\n크기는 경계마다 양쪽 값을 넣었다.\n0은 EINVAL이고 1은 성공한다.\n0xFFFFF00000(1 TiB - 1 MiB)까지는 성공하고, 1바이트만 더 커도 ENOSPC다.\n2^64-4095는 올림하다 넘쳐 0이 되니 EINVAL이다.\n가장 큰 값은 BO를 만들 때 같이 잡는 mmap 오프셋(나중에 mmap할 때 쓸 가짜 파일 위치) 공간의 크기다.\n메모리 512 MiB짜리 VM에서 이렇게 큰 BO가 만들어지는 건, 만들 때 페이지를 잡지 않기 때문이다.\n0과 2^64-4095를 막는 게 정말 크기 검사인지 보려고, 검사를 지우고 돌려 봤다.\n# RUN vnpu.bo_create_size_min ... # tools/vnpu-test.c:95:bo_create_size_min:Expected EINVAL (22) == errno (28) # bo_create_size_min: Test failed # FAIL vnpu.bo_create_size_min not ok 7 vnpu.bo_create_size_min # RUN vnpu.bo_create_size_offset_space ... # OK vnpu.bo_create_size_offset_space ok 8 vnpu.bo_create_size_offset_space # RUN vnpu.bo_create_size_wrap ... # tools/vnpu-test.c:117:bo_create_size_wrap:Expected EINVAL (22) == errno (28) # bo_create_size_wrap: Test failed # FAIL vnpu.bo_create_size_wrap not ok 9 vnpu.bo_create_size_wrap 검사가 없으면 두 값 다 ENOSPC(28)로 실패한다. 크기 0짜리 BO를 만들다 오프셋 구간을 못 잡아서다.\n잘못된 크기에는 EINVAL이 맞으니 검사는 되돌렸다.\nflags에 1을 넣으면 EINVAL이다.\n핸들은 fd마다 따로다. 첫 fd의 핸들을 다른 fd에서 닫으면 EINVAL이고, 첫 fd에서는 그대로 닫힌다.\n# PASSED: 12 / 12 tests passed. # Totals: pass:12 fail:0 xfail:0 xpass:0 skip:0 error:0 KASAN을 켠 커널인데 로그에 경고는 없었다.\nRepo: github.com/donggeunyoo/vnpu\n","permalink":"https://donggeunyoo.github.io/posts/vnpu-11/","summary":"\u003cp\u003e사용자 프로그램이 장치에 넘길 메모리를 만들 수 있게 했다.\u003cbr\u003e\n이 메모리 덩어리를 버퍼 객체(BO, buffer object)라고 한다. ivpu에서도 작업을 제출할 때 필요한 BO들을 핸들 배열로 넘긴다.\u003c/p\u003e\n\u003cp\u003eBO는 DRM의 메모리 관리자인 GEM(Graphics Execution Manager)이 관리한다.\u003cbr\u003e\nGEM은 BO 안에 뭐가 들었는지는 모르고, 사용자 공간에는 BO를 핸들이라는 번호로 보여 준다.\u003cbr\u003e\n핸들 표는 장치 파일을 open할 때마다 따로 생긴다.\u003c/p\u003e\n\u003cp\u003eBO 뒤의 메모리는 shmem 헬퍼로 만든다.\u003cbr\u003e\nshmem은 커널 안의 공유 메모리 파일 시스템이고, 사용자 공간에는 tmpfs로 보인다. 헬퍼는 BO마다 그 크기의 shmem 파일을 하나 만들어 둔다.\u003cbr\u003e\nivpu의 BO도 이 헬퍼 위에 있다.\u003c/p\u003e","title":"vnpu 개발기 #11 - 버퍼 객체 만들기"},{"content":"장치가 리셋될 때 IRQ_STATUS를 0으로 되돌리게 했다.\n9편에서 만든 이 레지스터는 게스트를 재부팅해도 값이 그대로 남아 있었다.\nmake run은 QEMU를 -no-reboot로 띄우는데, 이러면 게스트가 재부팅할 때 QEMU가 그냥 꺼진다.\n그래서 이 옵션만 빼고 QEMU를 직접 띄웠다.\n드라이버를 올리면 핸들러가 비트를 지우니까, 드라이버 없이 RAISE에 0x5를 쓰고 재부팅했다.\na는 5편과 같이 BAR 0의 시작 주소다.\n~ # devmem $((a+8)) 32 0x5 ~ # devmem $((a+4)) 32 0x00000005 ~ # reboot -f [ 14.562993] reboot: Restarting system [ 14.563494] reboot: machine restart 다시 부팅한 뒤 STATUS를 읽었다.\n~ # devmem $((a+4)) 32 0x00000005 재부팅 전에 세운 5가 그대로 남아 있다.\n게스트가 재부팅하면 QEMU는 기계 전체를 리셋하고, 버스 트리를 따라 모든 장치의 리셋 함수를 부른다.\nPCI 설정 공간의 COMMAND와 MSI 설정은 PCI 공통 코드가 지워 주지만, irq_status는 vnpu만 가진 값이라 vnpu가 지워야 한다.\n그런데 vnpu에는 리셋 함수가 없었다.\nQEMU의 리셋은 enter, hold, exit 세 단계로 나뉘고, 모든 장치의 enter가 끝나야 hold로 넘어간다.\nenter에서는 자기 상태만 되돌리고, 인터럽트를 올리거나 게스트 메모리에 쓰는 것처럼 바깥에 영향을 주는 일은 hold부터 할 수 있다.\nirq_status를 0으로 만드는 건 vnpu 안의 값만 바꾸는 일이라 enter 단계에 넣었다.\n고친 뒤 같은 순서로 다시 해 봤다.\n재부팅 전 STATUS는 똑같이 5였고, 재부팅 뒤에는 0이 나왔다.\n~ # devmem $((a+4)) 32 0x00000000 Repo: github.com/donggeunyoo/vnpu\n","permalink":"https://donggeunyoo.github.io/posts/vnpu-10/","summary":"\u003cp\u003e장치가 리셋될 때 IRQ_STATUS를 0으로 되돌리게 했다.\u003cbr\u003e\n9편에서 만든 이 레지스터는 게스트를 재부팅해도 값이 그대로 남아 있었다.\u003c/p\u003e\n\u003cp\u003emake run은 QEMU를 -no-reboot로 띄우는데, 이러면 게스트가 재부팅할 때 QEMU가 그냥 꺼진다.\u003cbr\u003e\n그래서 이 옵션만 빼고 QEMU를 직접 띄웠다.\u003cbr\u003e\n드라이버를 올리면 핸들러가 비트를 지우니까, 드라이버 없이 RAISE에 0x5를 쓰고 재부팅했다.\u003cbr\u003e\na는 5편과 같이 BAR 0의 시작 주소다.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e~ # devmem $((a+8)) 32 0x5\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e~ # devmem $((a+4)) 32\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e0x00000005\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e~ # reboot -f\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e[   14.562993] reboot: Restarting system\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e[   14.563494] reboot: machine restart\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e다시 부팅한 뒤 STATUS를 읽었다.\u003c/p\u003e","title":"vnpu 개발기 #10 - 장치 리셋"},{"content":"장치가 드라이버에게 인터럽트를 보낼 수 있게 하는 작업을 시작했다.\n앞으로 IPC나 job 완료 알림은 전부 인터럽트로 온다.\n인터럽트는 MSI로 보낸다.\nMSI는 인터럽트 전용 선 대신, 장치가 정해진 주소에 정해진 값을 메모리 쓰기하는 방식이다. QEMU에서도 MSI를 보내는 코드는 결국 메모리 쓰기 한 번이다.\n어떤 주소에 어떤 값을 쓸지는 운영체제가 정해서, 장치의 설정 공간에 있는 MSI capability에 적어 준다.\n그래서 첫 단계는 설정 공간에 MSI capability를 넣는 것이다.\nQEMU에서는 realize에서 msi_init 한 번이면 되고, 장치가 없어질 때는 msi_uninit으로 뺀다.\n벡터는 하나, 64비트 주소로 보낼 수 있게 했다.\n게스트에서 설정 공간을 직접 읽어 확인했다.\ncapability는 연결 리스트다. 오프셋 0x34(10진수 52)가 첫 항목의 위치를 가리키고, 각 항목의 첫 바이트가 종류, 둘째 바이트가 다음 항목의 위치다. MSI의 종류는 0x05다.\n~ # c=/sys/bus/pci/devices/0000:00:04.0/config ~ # p=$(od -An -tu1 -j52 -N1 $c) ~ # echo CAP_PTR $p CAP_PTR 64 ~ # od -An -tx1 -j$((p)) -N2 $c 05 00 0x40(64) 자리에 MSI(05)가 있고, 다음 위치가 00이라 capability는 이것 하나다.\n다음은 장치가 실제로 인터럽트를 올리게 하는 것이다. 레지스터 두 개를 새로 뒀다.\n0x04 IRQ_STATUS는 걸려 있는 인터럽트 비트다. 처리한 비트에 1을 쓰면 그 비트만 지워진다.\n0x08 IRQ_RAISE에 비트를 쓰면 장치가 그 비트를 STATUS에 세우고 MSI를 보낸다.\nMSI는 무슨 일이 일어났는지는 알려 주지 않는다. 드라이버가 인터럽트를 받고 STATUS를 읽어야 안다.\n1을 쓴 비트만 지우게 한 건, 드라이버가 여러 비트 중 처리한 것만 골라 지울 수 있게 하려는 것이다.\nIRQ_RAISE는 아직 인터럽트를 만들 진짜 일이 없어서, 드라이버가 인터럽트 경로를 시험할 수 있게 둔 레지스터다.\n드라이버가 MSI를 켜기 전에는 신호는 안 가고 비트만 남는다. 그래서 먼저 devmem으로 비트가 서고 지워지는지만 봤다.\na는 5편과 같이 BAR 0의 시작 주소다.\n~ # devmem $((a+4)) 32 0x00000000 ~ # devmem $((a+8)) 32 0x5 ~ # devmem $((a+4)) 32 0x00000005 ~ # devmem $((a+4)) 32 0x1 ~ # devmem $((a+4)) 32 0x00000004 ~ # devmem $((a+4)) 32 0x4 ~ # devmem $((a+4)) 32 0x00000000 RAISE에 0x5(비트 0, 2)를 쓰니 STATUS가 5가 됐고, 비트 0과 2를 하나씩 지우니 4, 0이 됐다.\n마지막으로 드라이버가 MSI를 켜고 인터럽트를 받게 했다. 드라이버가 할 일은 세 가지다.\npci_alloc_irq_vectors로 MSI 벡터를 하나 받는다. 이때 리눅스가 장치의 MSI capability에 보낼 주소와 값을 적고 MSI를 켠다.\ndevm_request_irq로 그 인터럽트에 핸들러를 건다. 핸들러는 STATUS를 읽고, 읽은 비트를 그대로 써서 지운다.\npci_set_master로 버스 마스터를 켠다. MSI는 장치가 메모리에 쓰는 동작인데, QEMU는 PCI COMMAND 레지스터의 버스 마스터 비트가 꺼져 있으면 장치의 메모리 쓰기를 막는다.\n드라이버를 올리고 RAISE에 1을 써서 장치가 인터럽트를 보내게 했다.\n~ # grep vnpu /proc/interrupts 24: 0 0 0 0 PCI-MSI-0000:00:04.0 0-edge vnpu ~ # devmem $((a+8)) 32 0x1 ~ # grep vnpu /proc/interrupts 24: 1 0 0 0 PCI-MSI-0000:00:04.0 0-edge vnpu ~ # devmem $((a+4)) 32 0x00000000 /proc/interrupts는 인터럽트마다 CPU별로 받은 횟수를 보여 준다. RAISE를 쓰기 전 0이던 횟수가 쓴 뒤 1이 됐다.\n마지막 STATUS가 0인 건 핸들러가 비트를 지웠기 때문이다.\n핸들러와 벡터는 둘 다 드라이버가 떨어질 때 자동으로 풀린다. 떼었다 다시 붙여도 인터럽트가 다시 오는지 봤다.\n~ # echo 0000:00:04.0 \u0026gt; /sys/bus/pci/drivers/vnpu/unbind ~ # grep -c vnpu /proc/interrupts 0 ~ # echo 0000:00:04.0 \u0026gt; /sys/bus/pci/drivers/vnpu/bind [ 14.161948] [drm] Initialized vnpu 1.0.0 for 0000:00:04.0 on minor 0 [ 14.162905] vnpu 0000:00:04.0: ID 0x564e5055 ~ # devmem $((a+8)) 32 0x1 ~ # grep vnpu /proc/interrupts 24: 1 0 0 0 PCI-MSI-0000:00:04.0 0-edge vnpu unbind 뒤엔 vnpu 인터럽트 줄이 없어졌고, 다시 bind하고 RAISE를 쓰니 새로 등록된 핸들러가 다시 한 번 받았다.\nRepo: github.com/donggeunyoo/vnpu\n","permalink":"https://donggeunyoo.github.io/posts/vnpu-09/","summary":"\u003cp\u003e장치가 드라이버에게 인터럽트를 보낼 수 있게 하는 작업을 시작했다.\u003cbr\u003e\n앞으로 IPC나 job 완료 알림은 전부 인터럽트로 온다.\u003c/p\u003e\n\u003cp\u003e인터럽트는 MSI로 보낸다.\u003cbr\u003e\nMSI는 인터럽트 전용 선 대신, 장치가 정해진 주소에 정해진 값을 메모리 쓰기하는 방식이다. QEMU에서도 MSI를 보내는 코드는 결국 메모리 쓰기 한 번이다.\u003cbr\u003e\n어떤 주소에 어떤 값을 쓸지는 운영체제가 정해서, 장치의 설정 공간에 있는 MSI capability에 적어 준다.\u003c/p\u003e\n\u003cp\u003e그래서 첫 단계는 설정 공간에 MSI capability를 넣는 것이다.\u003cbr\u003e\nQEMU에서는 realize에서 msi_init 한 번이면 되고, 장치가 없어질 때는 msi_uninit으로 뺀다.\u003cbr\u003e\n벡터는 하나, 64비트 주소로 보낼 수 있게 했다.\u003c/p\u003e","title":"vnpu 개발기 #9 - 인터럽트"},{"content":"사용자 공간 프로그램이 ioctl로 장치 정보를 물어볼 수 있게 했다.\n첫 항목은 PCI 디바이스 ID 하나다. ivpu도 첫 ioctl이 GET_PARAM이고, 첫 항목이 디바이스 ID다.\nioctl 번호와 주고받는 구조체는 드라이버와 사용자 공간이 똑같이 알아야 해서, 둘이 같이 include하는 uapi 헤더에 뒀다.\nstruct drm_vnpu_param { __u32 param; __u32 pad; __u64 value; }; value가 64비트라 구조체 크기를 64비트 배수로 맞추려고 pad를 명시했고, pad가 0이 아니면 드라이버가 거절한다.\n커널 문서(Documentation/process/botching-up-ioctls.rst)에 있는 규칙이다.\n드라이버 쪽은 ioctl 표에 함수 하나를 등록하는 게 전부다.\nDRM의 drm_ioctl이 번호로 표를 찾고, 구조체를 커널로 복사해 드라이버 함수를 부른 뒤, 결과를 다시 사용자 공간으로 복사해 준다.\n테스트는 커널 셀프테스트 하네스(kselftest_harness.h)로 짰다.\n처음 테스트 빌드는 이 에러로 깨졌다.\nkselftest_harness.h:640:52: error: comparison of integer expressions of different signedness: ‘int’ and ‘__u64’ {aka ‘long long unsigned int’} [-Werror=sign-compare] EXPECT_EQ(0x4e50, args.value)에서 0x4e50은 부호 있는 int이고 value는 부호 없는 __u64라서다. 0x4e50ULL로 타입을 맞췄다.\n테스트는 다섯 개다. 디바이스 ID, 모르는 param, 0이 아닌 pad, NULL 인자, 그리고 장치가 떨어진 뒤의 ioctl이다.\n마지막 테스트는 파일을 연 채로 unbind하고, 그 파일로 ioctl을 불러 ENODEV가 오는지 본다.\ndrm_ioctl이 맨 앞에서 장치가 뽑혔는지 검사해서, 드라이버 함수까지 가지 않는다.\n그런데 테스트를 끝내고 보니 장치 파일 이름이 바뀌어 있었다.\n~ # ls /dev/accel/ accel1 마지막 테스트는 끝날 때 다시 bind해서 원래대로 돌려놓는데, accel0이 아니라 accel1로 생겼다. 이러면 테스트를 한 번 더 돌릴 때 accel0을 못 연다.\n원인은 열어 둔 파일이었다. 파일이 옛 장치의 참조를 잡고 있는 동안은 옛 장치 객체가 해제되지 않고, 장치 번호 0도 반납되지 않는다. 그래서 새 장치가 1번을 받았다.\nbind 전에 파일을 먼저 닫게 고치니, 두 번 연달아 돌려도 둘 다 통과하고 장치도 accel0으로 돌아왔다.\n# PASSED: 5 / 5 tests passed. # PASSED: 5 / 5 tests passed. accel0 Repo: github.com/donggeunyoo/vnpu\n","permalink":"https://donggeunyoo.github.io/posts/vnpu-08/","summary":"\u003cp\u003e사용자 공간 프로그램이 ioctl로 장치 정보를 물어볼 수 있게 했다.\u003cbr\u003e\n첫 항목은 PCI 디바이스 ID 하나다. ivpu도 첫 ioctl이 GET_PARAM이고, 첫 항목이 디바이스 ID다.\u003c/p\u003e\n\u003cp\u003eioctl 번호와 주고받는 구조체는 드라이버와 사용자 공간이 똑같이 알아야 해서, 둘이 같이 include하는 uapi 헤더에 뒀다.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"n\"\u003edrm_vnpu_param\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003e__u32\u003c/span\u003e \u003cspan class=\"n\"\u003eparam\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003e__u32\u003c/span\u003e \u003cspan class=\"n\"\u003epad\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003e__u64\u003c/span\u003e \u003cspan class=\"n\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003evalue가 64비트라 구조체 크기를 64비트 배수로 맞추려고 pad를 명시했고, pad가 0이 아니면 드라이버가 거절한다.\u003cbr\u003e\n커널 문서(Documentation/process/botching-up-ioctls.rst)에 있는 규칙이다.\u003c/p\u003e\n\u003cp\u003e드라이버 쪽은 ioctl 표에 함수 하나를 등록하는 게 전부다.\u003cbr\u003e\nDRM의 drm_ioctl이 번호로 표를 찾고, 구조체를 커널로 복사해 드라이버 함수를 부른 뒤, 결과를 다시 사용자 공간으로 복사해 준다.\u003c/p\u003e","title":"vnpu 개발기 #8 - 첫 ioctl, GET_PARAM"},{"content":"드라이버를 accel 장치로 등록해서 /dev/accel/accel0을 만들었다.\n사용자 공간 프로그램이 NPU에 말을 걸 통로다. 앞으로 버퍼 할당이나 job 제출은 이 파일의 ioctl로 들어오게 된다.\nNPU 같은 계산 가속기는 리눅스 accel 서브시스템에 붙는다.\n뼈대는 GPU용 DRM 코어를 그대로 쓰고, 장치 파일만 /dev/dri가 아니라 /dev/accel 아래 생긴다.\n커널 문서(Documentation/accel/introduction.rst)에 따르면, 그래픽 쪽 사용자 공간 소프트웨어가 가속기를 GPU로 착각하지 않게 하려는 것이다.\n드라이버 쪽에서 할 일은 두 가지다.\ndrm_driver에 DRIVER_COMPUTE_ACCEL 플래그를 켜고, 파일 연산은 DEFINE_DRM_ACCEL_FOPS로 만든다.\nprobe에서는 ID 확인이 끝난 뒤 devm_drm_dev_alloc으로 DRM 장치를 할당하고, drm_dev_register로 등록한다. 여기서 장치 파일이 생긴다.\n처음 빌드는 실패했다.\ninclude/drm/drm_accel.h:26:27: error: ‘drm_ioctl’ undeclared here (not in a function); did you mean ‘drm_poll’? include/drm/drm_accel.h:27:27: error: ‘drm_compat_ioctl’ undeclared here (not in a function) include/drm/drm_accel.h:31:27: error: ‘drm_gem_mmap’ undeclared here (not in a function); did you mean ‘drm_get_cap’? DEFINE_DRM_ACCEL_FOPS가 펼쳐 넣는 함수들의 선언이 drm_accel.h 안에 없어서다.\ndrm_ioctl.h와 drm_gem.h를 직접 include해서 해결했다. ivpu도 이 둘을 같이 include한다.\n등록은 devm이 아니라서, 드라이버가 떨어질 때 풀어 줄 remove가 필요하다.\nremove에서는 drm_dev_unplug를 불렀다. 장치를 뽑혔음으로 표시하고, 진행 중인 접근이 끝나기를 기다린 뒤 등록을 푼다.\n결과는 이렇다.\n[ 13.838326] [drm] Initialized vnpu 1.0.0 for 0000:00:04.0 on minor 0 ~ # ls -l /dev/accel/ crw------- 1 0 0 261, 0 Sep 28 12:27 accel0 ~ # exec 3\u0026lt; /dev/accel/accel0 \u0026amp;\u0026amp; echo OPENED OPENED 261은 accel 장치에 따로 배정된 major 번호다.\n파일을 연 채로 장치가 떨어지는 경우도 돌려 봤다.\n~ # exec 3\u0026lt; /dev/accel/accel0 \u0026amp;\u0026amp; echo OPENED OPENED ~ # echo 0000:00:04.0 \u0026gt; /sys/bus/pci/drivers/vnpu/unbind ~ # ls /dev/accel/ ls: /dev/accel/: No such file or directory ~ # exec 3\u0026lt;\u0026amp;- ~ # echo CLOSED CLOSED 연 상태로 장치를 떨어뜨리고 나서, 3번으로 열어 둔 파일을 닫았다.\n이미 사라진 장치의 파일을 닫는 건데도 패닉 없이 CLOSED까지 정상으로 찍혔다.\n파일을 열 때 DRM 장치의 참조를 하나 잡고 닫을 때 놓기 때문에, 장치가 떨어져도 마지막 파일이 닫힐 때까지는 메모리가 살아 있다.\nRepo: github.com/donggeunyoo/vnpu\n","permalink":"https://donggeunyoo.github.io/posts/vnpu-07/","summary":"\u003cp\u003e드라이버를 accel 장치로 등록해서 /dev/accel/accel0을 만들었다.\u003cbr\u003e\n사용자 공간 프로그램이 NPU에 말을 걸 통로다. 앞으로 버퍼 할당이나 job 제출은 이 파일의 ioctl로 들어오게 된다.\u003c/p\u003e\n\u003cp\u003eNPU 같은 계산 가속기는 리눅스 accel 서브시스템에 붙는다.\u003cbr\u003e\n뼈대는 GPU용 DRM 코어를 그대로 쓰고, 장치 파일만 /dev/dri가 아니라 /dev/accel 아래 생긴다.\u003cbr\u003e\n커널 문서(Documentation/accel/introduction.rst)에 따르면, 그래픽 쪽 사용자 공간 소프트웨어가 가속기를 GPU로 착각하지 않게 하려는 것이다.\u003c/p\u003e\n\u003cp\u003e드라이버 쪽에서 할 일은 두 가지다.\u003cbr\u003e\ndrm_driver에 DRIVER_COMPUTE_ACCEL 플래그를 켜고, 파일 연산은 DEFINE_DRM_ACCEL_FOPS로 만든다.\u003cbr\u003e\nprobe에서는 ID 확인이 끝난 뒤 devm_drm_dev_alloc으로 DRM 장치를 할당하고, drm_dev_register로 등록한다. 여기서 장치 파일이 생긴다.\u003c/p\u003e","title":"vnpu 개발기 #7 - accel 장치로 등록하기"},{"content":"드라이버가 probe에서 ID 레지스터를 읽게 했다.\nID가 0x564e5055가 아니면 이 장치를 잡지 않는다.\n레지스터를 읽기 전에 두 가지가 필요하다.\npcim_enable_device로 장치를 켜고, pcim_iomap_region으로 BAR 0을 커널 주소에 매핑해야 드라이버가 readl로 읽을 수 있다.\npcim_iomap_region은 매핑과 함께 BAR 0 범위를 \u0026ldquo;vnpu\u0026rdquo; 이름으로 확보한다.\n둘 다 이름의 m이 managed라는 뜻이라, 드라이버가 떨어질 때 커널이 알아서 되돌린다.\nunbind했다가 다시 bind해서 정말 풀리는지 봤다.\n[ 13.877076] vnpu 0000:00:04.0: ID 0x564e5055 ~ # grep vnpu /proc/iomem febf1000-febf1fff : vnpu ~ # echo 0000:00:04.0 \u0026gt; /sys/bus/pci/drivers/vnpu/unbind ~ # grep -c vnpu /proc/iomem 0 ~ # echo 0000:00:04.0 \u0026gt; /sys/bus/pci/drivers/vnpu/bind [ 13.893207] vnpu 0000:00:04.0: ID 0x564e5055 풀어 주는 코드를 따로 안 썼는데, unbind하자 범위가 풀렸고 다시 bind해도 문제없이 잡혔다.\nID가 다를 때 정말 거절하는지는 QEMU의 ID 값을 잠깐 0x12345678로 바꿔서 확인했다.\n[ 14.084977] vnpu 0000:00:04.0: error -ENODEV: unexpected ID 0x12345678 ~ # ls /sys/bus/pci/drivers/vnpu/ bind module new_id remove_id uevent unbind ~ # grep -c vnpu /proc/iomem 0 드라이버는 장치를 잡지 않았고, ID를 읽기 전에 확보했던 BAR 범위도 풀려 있었다.\nRepo: github.com/donggeunyoo/vnpu\n","permalink":"https://donggeunyoo.github.io/posts/vnpu-06/","summary":"\u003cp\u003e드라이버가 probe에서 ID 레지스터를 읽게 했다.\u003cbr\u003e\nID가 0x564e5055가 아니면 이 장치를 잡지 않는다.\u003c/p\u003e\n\u003cp\u003e레지스터를 읽기 전에 두 가지가 필요하다.\u003cbr\u003e\npcim_enable_device로 장치를 켜고, pcim_iomap_region으로 BAR 0을 커널 주소에 매핑해야 드라이버가 readl로 읽을 수 있다.\u003c/p\u003e\n\u003cp\u003epcim_iomap_region은 매핑과 함께 BAR 0 범위를 \u0026ldquo;vnpu\u0026rdquo; 이름으로 확보한다.\u003cbr\u003e\n둘 다 이름의 m이 managed라는 뜻이라, 드라이버가 떨어질 때 커널이 알아서 되돌린다.\u003cbr\u003e\nunbind했다가 다시 bind해서 정말 풀리는지 봤다.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e[   13.877076] vnpu 0000:00:04.0: ID 0x564e5055\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e~ # grep vnpu /proc/iomem\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    febf1000-febf1fff : vnpu\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e~ # echo 0000:00:04.0 \u0026gt; /sys/bus/pci/drivers/vnpu/unbind\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e~ # grep -c vnpu /proc/iomem\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e0\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e~ # echo 0000:00:04.0 \u0026gt; /sys/bus/pci/drivers/vnpu/bind\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e[   13.893207] vnpu 0000:00:04.0: ID 0x564e5055\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e풀어 주는 코드를 따로 안 썼는데, unbind하자 범위가 풀렸고 다시 bind해도 문제없이 잡혔다.\u003c/p\u003e","title":"vnpu 개발기 #6 - 드라이버에서 ID 읽기"},{"content":"BAR 0 창에 첫 레지스터를 넣었다.\n오프셋 0x00을 읽으면 0x564e5055가 나오는 ID 레지스터다.\n드라이버가 장치를 잡고 가장 먼저 할 일은 이 창 너머에 정말 vnpu가 있는지 확인하는 것이다.\n창 연결이 어딘가 잘못돼 있으면 이 값이 안 나오니까, 이 값이 나오면 BAR 배치부터 매핑까지 제대로 됐다는 뜻이 된다.\n값은 16진수로 56 4e 50 55, ASCII로 \u0026ldquo;VNPU\u0026quot;다.\n게스트가 창을 읽으면 KVM이 그 접근을 QEMU로 넘기고, QEMU는 그 창의 MemoryRegionOps에 등록된 read 콜백을 부른다.\n콜백은 창 시작 기준 오프셋을 받아서, 0x00이면 ID를 돌려준다.\n레지스터는 전부 32비트라서 4바이트 접근만 받게 했다.\n읽기만 필요한 레지스터지만 write 콜백도 넣어야 했다.\nwrite 콜백 없이 게스트가 BAR 0 시작 주소(a)에 한 번 쓰게 했더니, QEMU가 바로 죽었다.\n~ # devmem $a 32 0x1 Thread 4 \u0026#34;CPU 1/KVM\u0026#34; received signal SIGSEGV, Segmentation fault. rip 0x0 0x0 #0 0x0000000000000000 in ??? () #1 0x0000555555da7f81 in memory_region_write_with_attrs_accessor (...) at ../system/memory.c:513 rip이 0이다. write 콜백이 비어 있으면 QEMU는 write_with_attrs 경로로 넘어가는데, 그것도 비어 있어서 NULL 함수 포인터를 부른 것이다.\n그래서 쓰기는 로그만 남기고 무시하는 콜백을 넣었다.\n드라이버는 아직 없어서, 게스트의 busybox devmem으로 물리 주소를 직접 읽고 써 봤다.\na에는 sysfs에서 읽은 BAR 0의 시작 물리 주소(0xfebf1000)를 넣었다.\n~ # a=$(head -1 /sys/bus/pci/devices/0000:00:04.0/resource | cut -d\u0026#34; \u0026#34; -f1) ~ # devmem $a 32 0x564E5055 ~ # devmem $((a+4)) 32 0x00000000 ~ # devmem $a 32 0x1 ~ # devmem $a 32 0x564E5055 ID는 그대로 나오고, 없는 레지스터는 0, 써도 QEMU는 멀쩡했다.\n0x04는 창 안이라 vnpu의 read 콜백이 받아서 0을 돌려준 것이다.\nRepo: github.com/donggeunyoo/vnpu\n","permalink":"https://donggeunyoo.github.io/posts/vnpu-05/","summary":"\u003cp\u003eBAR 0 창에 첫 레지스터를 넣었다.\u003cbr\u003e\n오프셋 0x00을 읽으면 0x564e5055가 나오는 ID 레지스터다.\u003c/p\u003e\n\u003cp\u003e드라이버가 장치를 잡고 가장 먼저 할 일은 이 창 너머에 정말 vnpu가 있는지 확인하는 것이다.\u003cbr\u003e\n창 연결이 어딘가 잘못돼 있으면 이 값이 안 나오니까, 이 값이 나오면 BAR 배치부터 매핑까지 제대로 됐다는 뜻이 된다.\u003cbr\u003e\n값은 16진수로 56 4e 50 55, ASCII로 \u0026ldquo;VNPU\u0026quot;다.\u003c/p\u003e\n\u003cp\u003e게스트가 창을 읽으면 KVM이 그 접근을 QEMU로 넘기고, QEMU는 그 창의 MemoryRegionOps에 등록된 read 콜백을 부른다.\u003cbr\u003e\n콜백은 창 시작 기준 오프셋을 받아서, 0x00이면 ID를 돌려준다.\u003cbr\u003e\n레지스터는 전부 32비트라서 4바이트 접근만 받게 했다.\u003c/p\u003e","title":"vnpu 개발기 #5 - 첫 레지스터"},{"content":"장치에 드라이버가 읽고 쓸 메모리 창을 달았다.\n지금까지 장치는 PCI 설정 공간, 그러니까 자기소개만 있었다.\nPCI 장치는 설정 공간의 BAR(Base Address Register)로 \u0026ldquo;이만한 크기의 메모리 창이 필요하다\u0026quot;고 알린다.\n부팅할 때 이 창이 물리 주소 공간 어딘가에 배치되고, 그 뒤로 CPU가 그 주소를 읽고 쓰면 RAM이 아니라 장치로 간다.\n이게 MMIO이고, 레지스터 접근은 전부 이 창을 거친다.\nQEMU에서는 이 창을 MemoryRegion으로 만들고 pci_register_bar로 BAR에 연결한다.\n크기는 4 KiB로 잡았다. CPU가 메모리를 페이지 단위로 매핑하니 이보다 작게 잡을 이유가 없었다.\n읽고 쓸 때 부를 콜백은 아직 안 넣었다. QEMU는 콜백이 없는 창을 어떤 접근도 받지 않는 영역으로 처리한다.\n부팅 로그에서 창이 잡힌 걸 확인했다.\npci 0000:00:04.0: BAR 0 [mem 0xfebf1000-0xfebf1fff] Repo: github.com/donggeunyoo/vnpu\n","permalink":"https://donggeunyoo.github.io/posts/vnpu-04/","summary":"\u003cp\u003e장치에 드라이버가 읽고 쓸 메모리 창을 달았다.\u003cbr\u003e\n지금까지 장치는 PCI 설정 공간, 그러니까 자기소개만 있었다.\u003c/p\u003e\n\u003cp\u003ePCI 장치는 설정 공간의 BAR(Base Address Register)로 \u0026ldquo;이만한 크기의 메모리 창이 필요하다\u0026quot;고 알린다.\u003cbr\u003e\n부팅할 때 이 창이 물리 주소 공간 어딘가에 배치되고, 그 뒤로 CPU가 그 주소를 읽고 쓰면 RAM이 아니라 장치로 간다.\u003cbr\u003e\n이게 MMIO이고, 레지스터 접근은 전부 이 창을 거친다.\u003c/p\u003e\n\u003cp\u003eQEMU에서는 이 창을 MemoryRegion으로 만들고 pci_register_bar로 BAR에 연결한다.\u003cbr\u003e\n크기는 4 KiB로 잡았다. CPU가 메모리를 페이지 단위로 매핑하니 이보다 작게 잡을 이유가 없었다.\u003cbr\u003e\n읽고 쓸 때 부를 콜백은 아직 안 넣었다. QEMU는 콜백이 없는 창을 어떤 접근도 받지 않는 영역으로 처리한다.\u003c/p\u003e","title":"vnpu 개발기 #4 - BAR 0 달기"},{"content":"드라이버보다 디바이스를 먼저 만들었다.\n드라이버가 probe할 대상이 없으면, 드라이버를 짜도 돌려 볼 방법이 없어서다.\nQEMU에서 PCI 디바이스를 만드는 건 PCI 설정 공간을 채우는 일부터 시작한다.\n리눅스는 부팅할 때 PCI 버스를 훑으면서 장치마다 벤더 ID, 디바이스 ID, 클래스 코드를 읽고, 그 값에 맞는 드라이버를 찾아 붙인다.\n벤더 ID는 QEMU가 자체 가상 장치에 쓰는 0x1234를 썼다.\n디바이스 ID는 0x4e50으로 직접 골랐다. 0x1234 안에서 비어 있는 번호이고, ASCII로 읽으면 \u0026ldquo;NP\u0026quot;다.\n클래스 코드는 처리 가속기를 뜻하는 0x1200이다.\n리눅스에는 이 값의 이름(PCI_CLASS_ACCELERATOR_PROCESSING)이 있는데 QEMU에는 없어서, 같은 이름으로 하나 추가했다.\n처음엔 hw/pci/pci.h만 include해서 빌드가 깨졌다.\nPCI 장치를 만들 때 쓰는 PCIDeviceClass와 TYPE_PCI_DEVICE는 hw/pci/pci_device.h에 있었다.\n참고한 edu는 hw/pci/msi.h를 통해 이 헤더를 간접적으로 받고 있어서, edu 코드만 봐서는 알 수 없었다.\n-device vnpu로 VM을 띄우니 게스트에서 이렇게 보였다.\n0x1234 0x4e50 0x120000 아직 레지스터도 메모리도 없는 빈 장치고, 잡는 드라이버도 없다.\n그다음 드라이버가 이 장치를 잡게 했다.\n리눅스 PCI 드라이버는 맡을 벤더/디바이스 ID를 테이블로 들고 PCI 코어에 등록하고, PCI 코어는 찾아 둔 장치가 테이블과 맞으면 드라이버의 probe를 불러 준다.\n테이블에 1234:4e50 하나를 적고, probe에서는 로그만 찍었다.\n[ 13.706517] vnpu 0000:00:04.0: probed ~ # ls /sys/bus/pci/drivers/vnpu/ 0000:00:04.0 module remove_id unbind insmod하자마자 드라이버가 00:04.0에 있는 장치에 붙었다.\nRepo: github.com/donggeunyoo/vnpu\n","permalink":"https://donggeunyoo.github.io/posts/vnpu-03/","summary":"\u003cp\u003e드라이버보다 디바이스를 먼저 만들었다.\u003cbr\u003e\n드라이버가 probe할 대상이 없으면, 드라이버를 짜도 돌려 볼 방법이 없어서다.\u003c/p\u003e\n\u003cp\u003eQEMU에서 PCI 디바이스를 만드는 건 PCI 설정 공간을 채우는 일부터 시작한다.\u003cbr\u003e\n리눅스는 부팅할 때 PCI 버스를 훑으면서 장치마다 벤더 ID, 디바이스 ID, 클래스 코드를 읽고, 그 값에 맞는 드라이버를 찾아 붙인다.\u003c/p\u003e\n\u003cp\u003e벤더 ID는 QEMU가 자체 가상 장치에 쓰는 0x1234를 썼다.\u003cbr\u003e\n디바이스 ID는 0x4e50으로 직접 골랐다. 0x1234 안에서 비어 있는 번호이고, ASCII로 읽으면 \u0026ldquo;NP\u0026quot;다.\u003cbr\u003e\n클래스 코드는 처리 가속기를 뜻하는 0x1200이다.\u003cbr\u003e\n리눅스에는 이 값의 이름(PCI_CLASS_ACCELERATOR_PROCESSING)이 있는데 QEMU에는 없어서, 같은 이름으로 하나 추가했다.\u003c/p\u003e","title":"vnpu 개발기 #3 - 빈 NPU 디바이스를 꽂고 드라이버로 잡기"},{"content":"NPU 디바이스를 어디에 만들지부터 정해야 했다.\n앞으로 만들 NPU 드라이버가 잡을 NPU는 QEMU에 없으므로, 드라이버를 돌리려면 디바이스 모델부터 직접 만들어야 한다.\n방법은 두 가지였다.\n하나는 QEMU 소스를 받아서 hw/misc 아래에 PCI 디바이스로 직접 넣는 것이고, 다른 하나는 libvfio-user로 디바이스를 별도 프로세스로 만들고 QEMU는 vfio-user로 붙이는 것이다.\nQEMU 포크로 정했다.\nQEMU에 디바이스가 들어가는 원래 방식 그대로라서다.\n대신 QEMU 전체를 소스에서 빌드해야 한다.\n기준은 QEMU 최신 안정판인 11.1.1로 잡았다.\nVM에서 돌릴 커널도 최신 릴리스인 7.2로 새로 빌드했다.\n이전 프로젝트에서 쓰던 커널은 accel 서브시스템(CONFIG_DRM_ACCEL)이 꺼져 있어서 그대로 쓸 수 없었다.\n이 둘로 VM을 띄우고, 로그만 찍는 빈 드라이버 모듈을 올렸다 내려 봤다.\n[ 14.027509] vnpu: loaded ~ # rmmod vnpu [ 14.029630] vnpu: unloaded 아직 디바이스는 없지만, make run 한 번으로 드라이버 빌드부터 VM 부팅까지 돈다.\nRepo: github.com/donggeunyoo/vnpu\n","permalink":"https://donggeunyoo.github.io/posts/vnpu-02/","summary":"\u003cp\u003eNPU 디바이스를 어디에 만들지부터 정해야 했다.\u003cbr\u003e\n앞으로 만들 NPU 드라이버가 잡을 NPU는 QEMU에 없으므로, 드라이버를 돌리려면 디바이스 모델부터 직접 만들어야 한다.\u003c/p\u003e\n\u003cp\u003e방법은 두 가지였다.\u003cbr\u003e\n하나는 QEMU 소스를 받아서 hw/misc 아래에 PCI 디바이스로 직접 넣는 것이고, 다른 하나는 libvfio-user로 디바이스를 별도 프로세스로 만들고 QEMU는 vfio-user로 붙이는 것이다.\u003c/p\u003e\n\u003cp\u003eQEMU 포크로 정했다.\u003cbr\u003e\nQEMU에 디바이스가 들어가는 원래 방식 그대로라서다.\u003cbr\u003e\n대신 QEMU 전체를 소스에서 빌드해야 한다.\u003cbr\u003e\n기준은 QEMU 최신 안정판인 11.1.1로 잡았다.\u003c/p\u003e\n\u003cp\u003eVM에서 돌릴 커널도 최신 릴리스인 7.2로 새로 빌드했다.\u003cbr\u003e\n이전 프로젝트에서 쓰던 커널은 accel 서브시스템(CONFIG_DRM_ACCEL)이 꺼져 있어서 그대로 쓸 수 없었다.\u003c/p\u003e","title":"vnpu 개발기 #2 - 개발 환경 세팅"},{"content":"이 프로젝트는 NPU 디바이스와 그 리눅스 드라이버를 밑바닥부터 직접 만드는 것이다.\n디바이스는 QEMU 안에 에뮬레이션 모델로 만들고, 드라이버는 그 디바이스를 잡는 accel 드라이버로 만든다.\n레퍼런스로는 인텔 NPU 드라이버인 drivers/accel/ivpu를 삼았다. 헤더 포함 소스 코드가 1만 5천 줄 정도 되는데, 이 드라이버가 할 수 있는 건 전부 할 수 있게 만드는 게 목표다.\nivpu는 NPU 드라이버가 어떤 일들을 처리해야 하는지 파악하는 용도로만 보고, 코드는 전부 새로 짠다.\n보면서 더 낫게 만들 수 있겠다 싶은 부분은 바꿔 볼 거고, 결국 ivpu랑 같은 방식을 택하게 되는 곳도 당연히 있을 수 있다.\n이 시리즈는 그 과정을 남기는 개발 기록이다.\n다 끝내고 정리해서 쓰는 게 아니라 작업하면서 그때그때 쓰는 거라, 삽질한 것도 그대로 남긴다.\n","permalink":"https://donggeunyoo.github.io/posts/vnpu-01/","summary":"\u003cp\u003e이 프로젝트는 NPU 디바이스와 그 리눅스 드라이버를 밑바닥부터 직접 만드는 것이다.\u003cbr\u003e\n디바이스는 QEMU 안에 에뮬레이션 모델로 만들고, 드라이버는 그 디바이스를 잡는 accel 드라이버로 만든다.\u003c/p\u003e\n\u003cp\u003e레퍼런스로는 인텔 NPU 드라이버인 drivers/accel/ivpu를 삼았다. 헤더 포함 소스 코드가 1만 5천 줄 정도 되는데, 이 드라이버가 할 수 있는 건 전부 할 수 있게 만드는 게 목표다.\u003cbr\u003e\nivpu는 NPU 드라이버가 어떤 일들을 처리해야 하는지 파악하는 용도로만 보고, 코드는 전부 새로 짠다.\u003cbr\u003e\n보면서 더 낫게 만들 수 있겠다 싶은 부분은 바꿔 볼 거고, 결국 ivpu랑 같은 방식을 택하게 되는 곳도 당연히 있을 수 있다.\u003c/p\u003e","title":"vnpu 개발기 #1 - NPU 디바이스와 드라이버를 밑바닥부터 만들기"},{"content":"I contribute to the Linux kernel, mostly in drivers/accel, drivers/iommu, and drivers/pci.\nGitHub: donggeunyoo Email: donggeunyoo.kernel@gmail.com ","permalink":"https://donggeunyoo.github.io/about/","summary":"About","title":"About"}]