# process_vm_readv/writev 👥
관련 소스:mm/process_vm_access.c,include/linux/uio.h,include/linux/mm_types.h
process_vm_readv과 process_vm_writev는 프로세스 간 메모리 직접 접근을 위한 시스템 호출입니다. 기존의 ptrace(PTRACE_PEEKDATA)나 파이프/소켓을 통한 간접 전송과 달리, 커널이 두 프로세스의 가상 주소 공간에 직접 접근하여 데이터를 복사합니다. 이를 통해 상대방 프로세스의 가상 주소를 하나의 시스템 호출로 여러 번 읽거나 쓸 수 있어 성능과 편의성이 향상됩니다.
시스템 호출은 사용자 제공 iovec 배열을 통해 로컬과 원격 주소 영역을 지정하며, 커널 내부에서 pin_user_pages_remote()를 사용하여 원격 프로세스의 페이지를 고정(pinning)한 후 복사합니다. 이 과정에서 mm_access()를 통해 대상 프로세스의 mm_struct에 접근 권한을 얻어야 하며, PTRACE_MODE_ATTACH_REALCREDS 플래그를 사용합니다.
/* mm/process_vm_access.c — 시스템 호출 정의 (라인 292-305) */
SYSCALL_DEFINE6(process_vm_readv, pid_t, pid, const struct iovec __user *, lvec,
unsigned long, liovcnt, const struct iovec __user *, rvec,
unsigned long, riovcnt, unsigned long, flags)
{
return process_vm_rw(pid, lvec, liovcnt, rvec, riovcnt, flags, 0);
}
SYSCALL_DEFINE6(process_vm_writev, pid_t, pid,
const struct iovec __user *, lvec,
unsigned long, liovcnt, const struct iovec __user *, rvec,
unsigned long, riovcnt, unsigned long, flags)
{
return process_vm_rw(pid, lvec, liovcnt, rvec, riovcnt, flags, 1);
}
# 대상 프로세스의 메모리 매핑 확인
cat /proc/<pid>/maps
# process_vm_readv/writev 시스템 호출 추적
strace -e process_vm_readv,process_vm_writev <command>
# 현재 프로세스의 RSS 확인
cat /proc/self/status | grep VmRSS
# 커널 로그에서 관련 fault 메시지 확인
dmesg | grep -i "fault\|page"
# 시스템 호출 통계 확인
cat /proc/self/syscall
# 대상 프로세스의 메모리 사용량 모니터링
top -p <pid>
# iovec 구조체 확인 (디버깅용)
strace -v -e process_vm_readv <command>
# 커널 메모리 할당 통계
cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable"
# 프로세스의 열린 파일 디스크립터 확인
ls -la /proc/<pid>/fd
# 대상 프로세스의 mm_struct 주소 확인 (crash 도구 사용 시)
crash> task_struct.mm <pid>
다양한 유형의 벡터(iov, kvec, bvec 등)를 추상화하여 통일된 인터페이스를 제공합니다. process_vm_access.c에서는 로컬과 원격 주소 영역을 순회하는 데 사용됩니다.
/* include/linux/uio.h:43-83 */
struct iov_iter {
u8 iter_type; /* 반복자 유형 (ITER_IOVEC 등) */
bool nofault; /* 폴트 없이 처리할지 여부 */
bool data_source; /* 데이터 소스 방향 (읽기/쓰기) */
size_t iov_offset; /* 현재 벡터 내 오프셋 */
union {
struct iovec __ubuf_iovec;
struct {
union {
const struct iovec *__iov; /* iovec 배열 포인터 */
const struct kvec *kvec;
const struct bio_vec *bvec;
const struct folio_queue *folioq;
struct xarray *xarray;
void __user *ubuf;
};
size_t count; /* 남은 바이트 수 */
};
};
union {
unsigned long nr_segs; /* 세그먼트 수 */
u8 folioq_slot;
loff_t xarray_start;
};
};
프로세스의 가상 주소 공간, 페이지 테이블, VMA 목록 등을 관리하는 핵심 구조체입니다. process_vm_rw_core()에서 mm_access()를 통해 획득합니다.
/* include/linux/mm_types.h:1123-1240 (일부) */
struct mm_struct {
struct {
struct {
atomic_t mm_count; /* mm_struct 참조 카운트 */
} ____cacheline_aligned_in_smp;
struct maple_tree mm_mt; /* VMA 저장 (maple tree) */
unsigned long mmap_base; /* mmap 영역 기준 주소 */
unsigned long mmap_legacy_base; /* 아래쪽 성장 mmap 기준 주소 */
unsigned long task_size; /* 사용자 주소 공간 크기 */
pgd_t *pgd; /* 페이지 글로벌 디렉토리 */
atomic_t mm_users; /* 사용자 수 (mmget/mmput) */
int map_count; /* VMA 개수 */
spinlock_t page_table_lock; /* 페이지 테이블 보호 */
struct rw_semaphore mmap_lock; /* mmap 보호 (읽기/쓰기 잠금) */
unsigned long total_vm; /* 총 매핑된 페이지 수 */
unsigned long locked_vm; /* 고정된 페이지 수 */
atomic64_t pinned_vm; /* 영구 고정 페이지 수 */
};
/* ... */
};
process_vm_rw_core()에서 find_get_task_by_vpid()를 통해 대상 프로세스의 task_struct를 획득합니다. 이를 통해 mm_struct에 접근할 수 있습니다.
/* include/linux/sched.h:952-959 */
struct sched_info sched_info;
struct list_head tasks;
struct plist_node pushable_tasks;
struct rb_node pushable_dl_tasks;
struct mm_struct *mm;
struct mm_struct *active_mm;
사용자 공간에서 process_vm_readv/writev에 전달되는 주소-길이 쌍입니다.
/* include/uapi/linux/uio.h */
struct iovec {
void __user *iov_base; /* 시작 주소 */
size_t iov_len; /* 길이 (바이트) */
};
/* mm/process_vm_access.c:56-59 */
#define PVM_MAX_KMALLOC_PAGES 2
#define PVM_MAX_USER_PAGES (PVM_MAX_KMALLOC_PAGES * PAGE_SIZE / sizeof(struct page *))
/* mm/process_vm_access.c:136 */
#define PVM_MAX_PP_ARRAY_COUNT 16
process_vm_rw()를 호출하고 vm_write 플래그로 읽기(0) 또는 쓰기(1)를 지정합니다.
pid, lvec(로컬), liovcnt, rvec(원격), riovcnt, flagsflags != 0이면 -EINVAL 반환사용자 제공 iovec 배열을 커널 공간으로 가져오고 import_iovec()로 iov_iter를 초기화합니다.
/* mm/process_vm_access.c:254-290 */
static ssize_t process_vm_rw(pid_t pid,
const struct iovec __user *lvec,
unsigned long liovcnt,
const struct iovec __user *rvec,
unsigned long riovcnt,
unsigned long flags, int vm_write)
{
/* ... */
int dir = vm_write ? ITER_SOURCE : ITER_DEST;
if (flags != 0)
return -EINVAL;
/* 로컬 iovec을 iov_iter로 변환 */
rc = import_iovec(dir, lvec, liovcnt, UIO_FASTIOV, &iov_l, &iter);
if (rc < 0)
return rc;
if (!iov_iter_count(&iter))
goto free_iov_l;
/* 원격 iovec을 커널 공간으로 복사 */
iov_r = iovec_from_user(rvec, riovcnt, UIO_FASTIOV, iovstack_r,
in_compat_syscall());
if (IS_ERR(iov_r)) {
rc = PTR_ERR(iov_r);
goto free_iov_l;
}
rc = process_vm_rw_core(pid, &iter, iov_r, riovcnt, flags, vm_write);
/* ... */
}
- flags != 0: -EINVAL 반환 (현재 미사용)
- iov_iter_count(&iter) == 0: 바로 반환
- import_iovec() 실패: 오류 코드 반환
대상 프로세스를 찾고, mm_struct에 접근한 후, 모든 원격 벡터에 대해 process_vm_rw_single_vec()을 호출합니다.
/* mm/process_vm_access.c:151-238 (일부) */
static ssize_t process_vm_rw_core(pid_t pid, struct iov_iter *iter,
const struct iovec *rvec,
unsigned long riovcnt,
unsigned long flags, int vm_write)
{
struct task_struct *task;
struct page *pp_stack[PVM_MAX_PP_ARRAY_COUNT];
struct page **process_pages = pp_stack;
struct mm_struct *mm;
/* ... */
/* 대상 프로세스 찾기 */
task = find_get_task_by_vpid(pid);
if (!task) {
rc = -ESRCH;
goto free_proc_pages;
}
/* mm_struct 접근 권한 획득 */
mm = mm_access(task, PTRACE_MODE_ATTACH_REALCREDS);
if (IS_ERR(mm)) {
rc = PTR_ERR(mm);
if (rc == -EACCES)
rc = -EPERM; /* EACCES를 EPERM으로 변환 */
goto put_task_struct;
}
/* 각 원격 벡터에 대해 처리 */
for (i = 0; i < riovcnt && iov_iter_count(iter) && !rc; i++)
rc = process_vm_rw_single_vec(
(unsigned long)rvec[i].iov_base, rvec[i].iov_len,
iter, process_pages, mm, task, vm_write);
/* 전송된 바이트 수 계산 */
total_len -= iov_iter_count(iter);
if (total_len)
rc = total_len;
mmput(mm);
put_task_struct:
put_task_struct(task);
free_proc_pages:
if (process_pages != pp_stack)
kfree(process_pages);
return rc;
}
- find_get_task_by_vpid() 실패: -ESRCH
- mm_access() 실패: -EACCES → -EPERM 변환
- nr_pages == 0: 바로 반환
하나의 원격 벡터에 대해 페이지를 고정(pinning)하고 복사합니다. pin_user_pages_remote()를 사용하여 원격 프로세스의 페이지를 안전하게 고정합니다.
/* mm/process_vm_access.c:73-132 (일부) */
static int process_vm_rw_single_vec(unsigned long addr,
unsigned long len,
struct iov_iter *iter,
struct page **process_pages,
struct mm_struct *mm,
struct task_struct *task,
int vm_write)
{
unsigned long pa = addr & PAGE_MASK;
unsigned long start_offset = addr - pa;
/* ... */
if (vm_write)
flags |= FOLL_WRITE;
while (!rc && nr_pages && iov_iter_count(iter)) {
int pinned_pages = min_t(unsigned long, nr_pages, PVM_MAX_USER_PAGES);
/* 원격 프로세스의 페이지 고정 */
mmap_read_lock(mm);
pinned_pages = pin_user_pages_remote(mm, pa, pinned_pages,
flags, process_pages,
&locked);
if (locked)
mmap_read_unlock(mm);
if (pinned_pages <= 0)
return -EFAULT;
/* 페이지 복사 */
rc = process_vm_rw_pages(process_pages,
start_offset, bytes, iter,
vm_write);
/* 쓰기 시 페이지 더러운 플래그 설정 */
unpin_user_pages_dirty_lock(process_pages, pinned_pages,
vm_write);
}
return rc;
}
- vm_write == 1: FOLL_WRITE 플래그 추가
- pinned_pages <= 0: -EFAULT
- locked == 1: mmap_read_unlock() 호출
고정된 페이지들의 내용을 copy_page_to_iter() 또는 copy_page_from_iter()를 통해 복사합니다.
/* mm/process_vm_access.c:27-53 */
static int process_vm_rw_pages(struct page **pages,
unsigned offset,
size_t len,
struct iov_iter *iter,
int vm_write)
{
while (len && iov_iter_count(iter)) {
struct page *page = *pages++;
size_t copy = PAGE_SIZE - offset;
size_t copied;
if (copy > len)
copy = len;
if (vm_write)
copied = copy_page_from_iter(page, offset, copy, iter);
else
copied = copy_page_to_iter(page, offset, copy, iter);
len -= copied;
if (copied < copy && iov_iter_count(iter))
return -EFAULT;
offset = 0;
}
return 0;
}
- vm_write == 1: copy_page_from_iter() (원격 → 로컬)
- vm_write == 0: copy_page_to_iter() (로컬 → 원격)
- copied < copy: -EFAULT
process_vm_readv / process_vm_writev
│
└── process_vm_rw()
│
├── import_iovec() ← 로컬 iovec → iov_iter 변환
├── iovec_from_user() ← 원격 iovec을 커널 공간으로 복사
│
└── process_vm_rw_core()
│
├── find_get_task_by_vpid() ← 대상 프로세스 찾기
├── mm_access() ← mm_struct 접근 권한 획득
│
└── process_vm_rw_single_vec() ← 각 원격 벡터별 반복
│
├── pin_user_pages_remote() ← 원격 페이지 고정
├── process_vm_rw_pages() ← 실제 데이터 복사
│ ├── copy_page_to_iter() (읽기)
│ └── copy_page_from_iter() (쓰기)
└── unpin_user_pages_dirty_lock() ← 페이지 고정 해제
| 항목 | process_vm_readv | process_vm_writev |
|---|---|---|
| **방향** | 원격 → 로컬 | 로컬 → 원격 |
| **vm_write** | 0 | 1 |
| **FOLL_WRITE** | 미설정 | 설정 |
| ** iov_iter 방향** | ITER_DEST | ITER_SOURCE |
| **페이지 더러움** | 설정 안 함 | unpin 시 더러운 플래그 설정 |
| **EACCES 처리** | EACCES → EPERM 변환 | EACCES → EPERM 변환 |
| 항목 | process_vm_readv/writev | ptrace(PTRACE_PEEKDATA) | 파이프 |
|---|---|---|---|
| **동시 접근** | 단일 시스템 호출로 다수 벡터 처리 | 1회 호출당 1 워드 | 양방향 가능 |
| **성능** | 높음 (페이지 고정 후 복사) | 낮음 (컨텍스트 스위치 반복) | 중간 |
| **권한** | PTRACE_MODE_ATTACH_REALCREDS | PTRACE_MODE_ATTACH | 두 프로세스간 파이프 필요 |
| **대상 프로세스** | PID로 지정 | PID로 지정 | 파일 디스크립터 필요 |
| 조건 | 결과 |
|---|---|
| `flags != 0` | `-EINVAL` 반환 |
| `find_get_task_by_vpid()` 실패 | `-ESRCH` 반환 |
| `mm_access()` 실패 시 `-EACCES` | `-EPERM`으로 변환 |
| `pin_user_pages_remote() <= 0` | `-EFAULT` 반환 |
| `nr_pages == 0` | 바로 0 반환 |
| `iov_iter_count(&iter) == 0` | 바로 반환 |
process_vm_readv/writev는 두 이웃집 사이에 직접 통로를 뚫는 것과 같습니다. 기존 방법(파이프/소켓)은 우체국(커널)을 통해 편지를 주고받는 방식이라면, 이 시스템 호출은 이웃집 문을 열고 상대방 선반에서 직접 물건을 가져오거나 넣어두는 방식입니다. 단, 보안(권한 검사)이 필요하며, 한 번에 가져올 수 있는 물건의 양(페이지 수)에 제한이 있습니다.