Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

# process_vm_readv/writev 👥

관련 소스: mm/process_vm_access.c, include/linux/uio.h, include/linux/mm_types.h

개요 (Overview)

process_vm_readvprocess_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>

핵심 자료구조

struct iov_iter — I/O 벡터 반복자

다양한 유형의 벡터(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;
	};
};

struct mm_struct — 프로세스 메모리 관리 구조체

프로세스의 가상 주소 공간, 페이지 테이블, 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;            /* 영구 고정 페이지 수 */
	};
	/* ... */
};

struct task_struct — 태스크 구조체 (발췌)

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;

struct iovec — 사용자 공간 I/O 벡터

사용자 공간에서 process_vm_readv/writev에 전달되는 주소-길이 쌍입니다.

/* include/uapi/linux/uio.h */
struct iovec {
	void __user *iov_base;    /* 시작 주소 */
	size_t iov_len;           /* 길이 (바이트) */
};

PVM_MAX_* 매크로 — 내부 제한값

/* 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
자료구조 관계도

핵심 함수

1. SYSCALL_DEFINE6(process_vm_readv/writev) — 시스템 호출 진입점

process_vm_rw()를 호출하고 vm_write 플래그로 읽기(0) 또는 쓰기(1)를 지정합니다.

  • 입력: pid, lvec(로컬), liovcnt, rvec(원격), riovcnt, flags
  • 반환: 전송된 바이트 수 또는 오류 코드
  • 조건 분기: flags != 0이면 -EINVAL 반환
  • 2. process_vm_rw() — iovec 검증 및 코어 호출

    사용자 제공 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() 실패: 오류 코드 반환

    3. process_vm_rw_core() — 코어 처리 루틴

    대상 프로세스를 찾고, 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: 바로 반환

    4. process_vm_rw_single_vec() — 단일 벡터 처리

    하나의 원격 벡터에 대해 페이지를 고정(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() 호출

    5. process_vm_rw_pages() — 실제 페이지 복사

    고정된 페이지들의 내용을 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_readvprocess_vm_writev
    **방향**원격 → 로컬로컬 → 원격
    **vm_write**01
    **FOLL_WRITE**미설정설정
    ** iov_iter 방향**ITER_DESTITER_SOURCE
    **페이지 더러움**설정 안 함unpin 시 더러운 플래그 설정
    **EACCES 처리**EACCES → EPERM 변환EACCES → EPERM 변환
    항목process_vm_readv/writevptrace(PTRACE_PEEKDATA)파이프
    **동시 접근**단일 시스템 호출로 다수 벡터 처리1회 호출당 1 워드양방향 가능
    **성능**높음 (페이지 고정 후 복사)낮음 (컨텍스트 스위치 반복)중간
    **권한**PTRACE_MODE_ATTACH_REALCREDSPTRACE_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는 두 이웃집 사이에 직접 통로를 뚫는 것과 같습니다. 기존 방법(파이프/소켓)은 우체국(커널)을 통해 편지를 주고받는 방식이라면, 이 시스템 호출은 이웃집 문을 열고 상대방 선반에서 직접 물건을 가져오거나 넣어두는 방식입니다. 단, 보안(권한 검사)이 필요하며, 한 번에 가져올 수 있는 물건의 양(페이지 수)에 제한이 있습니다.


    관련 문서

  • 메모리 관리 개요
  • Buddy Allocator
  • VMA / mmap
  • GUP (Get User Pages)