struct iov_iter & readv()

dandb3·2024년 8월 5일
0

linux kernel

목록 보기
12/21

io_uring에 대해 공부하다가 struct iov_iter와 관련된 아는 내용이 없어서 이쪽을 찾아보기로 했다.

개요

리눅스에서는 일반적으로 read / write를 할 때 read/write(fd, buf, size); 의 형태로 호출한다.
하지만 이 외에도 벡터 형태의 I/O 기능 또한 지원하는데, 그것을 알아보도록 하자.

struct iovec

struct iovec
{
	void __user *iov_base;	/* BSD uses caddr_t (1003.1g requires void *) */
	__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};

struct iovec은 벡터의 원소 하나를 의미하는 구조체이다.

  • iov_base : 버퍼의 주소
  • iov_len : 버퍼의 길이

struct iov_iter

struct iov_iter {
	u8 iter_type;
	bool nofault;
	bool data_source;
	bool user_backed;
	union {
		size_t iov_offset;
		int last_offset;
	};
	size_t count;
	union {
		const struct iovec *iov;
		const struct kvec *kvec;
		const struct bio_vec *bvec;
		struct xarray *xarray;
		struct pipe_inode_info *pipe;
		void __user *ubuf;
	};
	union {
		unsigned long nr_segs;
		struct {
			unsigned int head;
			unsigned int start_head;
		};
		loff_t xarray_start;
	};
};
  • iter_type : struct iov_iter가 참조하는 버퍼의 종류를 나타낸다.
  • data_source : 현재 struct iov_iterREAD에 사용되는지, WRITE에 사용되는 것인지 구분한다.
  • iov_offset : 현재까지 작업 한 후의 iovec 상에서의 offset을 의미한다.
  • count : 요청한 I/O의 남은 크기를 의미한다.
  • iov(를 비롯한 union들) : I/O에 쓰이는 array를 의미한다.
  • nr_segs(를 비롯한 union들) : array의 크기를 의미한다.

sys_readv()

readv() 시스템 콜에 대해 알아보자.

SYSCALL_DEFINE3(readv, unsigned long, fd, const struct iovec __user *, vec,
		unsigned long, vlen)
{
	return do_readv(fd, vec, vlen, 0);
}

인자로 iovec 배열, 배열의 크기를 받는다.

do_readv()

static ssize_t do_readv(unsigned long fd, const struct iovec __user *vec,
			unsigned long vlen, rwf_t flags)
{
	struct fd f = fdget_pos(fd);
	ssize_t ret = -EBADF;

	if (f.file) {
		loff_t pos, *ppos = file_ppos(f.file);
		if (ppos) {
			pos = *ppos;
			ppos = &pos;
		}
		ret = vfs_readv(f.file, vec, vlen, ppos, flags);
		if (ret >= 0 && ppos)
			f.file->f_pos = pos;
		fdput_pos(f);
	}

	if (ret > 0)
		add_rchar(current, ret);
	inc_syscr(current);
	return ret;
}

내부적으로 바로 vfs_readv()를 호출한다.

vfs_readv()

static ssize_t vfs_readv(struct file *file, const struct iovec __user *vec,
		  unsigned long vlen, loff_t *pos, rwf_t flags)
{
	struct iovec iovstack[UIO_FASTIOV];
	struct iovec *iov = iovstack;
	struct iov_iter iter;
	ssize_t ret;

	ret = import_iovec(READ, vec, vlen, ARRAY_SIZE(iovstack), &iov, &iter);
	if (ret >= 0) {
		ret = do_iter_read(file, &iter, pos, flags);
		kfree(iov);
	}

	return ret;
}

더 빠른 처리를 위해 UIO_FASTIOV 크기의 iovec 배열을 스택에 만들어 놓는다.
import_iovec()을 호출해서 user의 iovec을 커널에 복사해 온 후 iter을 초기화 해 준다.
ret >= 0인 경우(요청한 크기가 0 이상일 경우 && 에러 발생 x)
do_iter_read()를 호출해서 실제 read 작업을 수행하게 된다.

import_iovec()

ssize_t import_iovec(int type, const struct iovec __user *uvec,
		 unsigned nr_segs, unsigned fast_segs,
		 struct iovec **iovp, struct iov_iter *i)
{
	return __import_iovec(type, uvec, nr_segs, fast_segs, iovp, i,
			      in_compat_syscall());
}

__import_iovec()

ssize_t __import_iovec(int type, const struct iovec __user *uvec,
		 unsigned nr_segs, unsigned fast_segs, struct iovec **iovp,
		 struct iov_iter *i, bool compat)
{
	ssize_t total_len = 0;
	unsigned long seg;
	struct iovec *iov;

	iov = iovec_from_user(uvec, nr_segs, fast_segs, *iovp, compat);
	if (IS_ERR(iov)) {
		*iovp = NULL;
		return PTR_ERR(iov);
	}

	/*
	 * According to the Single Unix Specification we should return EINVAL if
	 * an element length is < 0 when cast to ssize_t or if the total length
	 * would overflow the ssize_t return value of the system call.
	 *
	 * Linux caps all read/write calls to MAX_RW_COUNT, and avoids the
	 * overflow case.
	 */
	for (seg = 0; seg < nr_segs; seg++) {
		ssize_t len = (ssize_t)iov[seg].iov_len;

		if (!access_ok(iov[seg].iov_base, len)) {
			if (iov != *iovp)
				kfree(iov);
			*iovp = NULL;
			return -EFAULT;
		}

		if (len > MAX_RW_COUNT - total_len) {
			len = MAX_RW_COUNT - total_len;
			iov[seg].iov_len = len;
		}
		total_len += len;
	}

	iov_iter_init(i, type, iov, nr_segs, total_len);
	if (iov == *iovp)
		*iovp = NULL;
	else
		*iovp = iov;
	return total_len;
}
  • iovec_from_user() 를 통해 iovec을 user로 부터 복사해 온다.
  • for문을 돌면서 전체 요청한 길이가 MAX_RW_COUNT를 넘지 않는지 확인하고, 그 길이를 total_len에 저장한다.
  • iov_iter의 값을 초기화한다.
  • total_len을 리턴한다.

여기서 마지막 if문은 왜 있을까?
vfs_readv()로 돌아가서 보면, do_iter_read()를 통해 read 작업을 완료한 후 kfree(iov)를 통해 할당했던 메모리를 해제해 주는 부분이 있다.
이 때, fast_iov를 사용하지 않았을 때에만 메모리 할당해제를 해 주어야 하므로 앞서서 fast_iov를 사용한 경우에는 *iovp = NULL 처리를 해 주어야 하는 것이다.

iovec_from_user()

struct iovec *iovec_from_user(const struct iovec __user *uvec,
		unsigned long nr_segs, unsigned long fast_segs,
		struct iovec *fast_iov, bool compat)
{
	struct iovec *iov = fast_iov;
	int ret;

	/*
	 * SuS says "The readv() function *may* fail if the iovcnt argument was
	 * less than or equal to 0, or greater than {IOV_MAX}.  Linux has
	 * traditionally returned zero for zero segments, so...
	 */
	if (nr_segs == 0)
		return iov;
	if (nr_segs > UIO_MAXIOV)
		return ERR_PTR(-EINVAL);
	if (nr_segs > fast_segs) {
		iov = kmalloc_array(nr_segs, sizeof(struct iovec), GFP_KERNEL);
		if (!iov)
			return ERR_PTR(-ENOMEM);
	}

	if (unlikely(compat))
		ret = copy_compat_iovec_from_user(iov, uvec, nr_segs);
	else
		ret = copy_iovec_from_user(iov, uvec, nr_segs);
	if (ret) {
		if (iov != fast_iov)
			kfree(iov);
		return ERR_PTR(ret);
	}

	return iov;
}

앞서 스택에 만들어 놓았던 fast_iov를 사용할 수 있는지 여부를 판단한다.
만약 nr_segs의 크기가 fast_segs보다 크다면 사용할 수 없는 것이므로, 이 때는 kmalloc_array()를 사용하여 메모리를 할당한다.

그 후 copy_iovec_from_user()를 통해서 iov에 data를 복사해 넣는다.

copy_iovec_from_user()

static __noclone int copy_iovec_from_user(struct iovec *iov,
		const struct iovec __user *uiov, unsigned long nr_segs)
{
	int ret = -EFAULT;

	if (!user_access_begin(uiov, nr_segs * sizeof(*uiov)))
		return -EFAULT;

	do {
		void __user *buf;
		ssize_t len;

		unsafe_get_user(len, &uiov->iov_len, uaccess_end);
		unsafe_get_user(buf, &uiov->iov_base, uaccess_end);

		/* check for size_t not fitting in ssize_t .. */
		if (unlikely(len < 0)) {
			ret = -EINVAL;
			goto uaccess_end;
		}
		iov->iov_base = buf;
		iov->iov_len = len;

		uiov++; iov++;
	} while (--nr_segs);

	ret = 0;
uaccess_end:
	user_access_end();
	return ret;
}

새로운 kernel에서의 iov에 userland에서의 iov값들을 복사한다.
여기서 주의할 점은, buffer 내부의 내용들을 그대로 복사하는 것이 아닌, buf 주소와 len 값만 복사해 놓는다는 점이다.
아마 내 생각에는 최적화를 위해 복사의 양을 줄인 게 아닌가 싶다.

나중에 실제 read가 일어나는 부분에서 copy_to_user()를 호출하는지 확인해야 할 것 같음.

iov_iter_init()

void iov_iter_init(struct iov_iter *i, unsigned int direction,
			const struct iovec *iov, unsigned long nr_segs,
			size_t count)
{
	WARN_ON(direction & ~(READ | WRITE));
	*i = (struct iov_iter) {
		.iter_type = ITER_IOVEC,
		.nofault = false,
		.user_backed = true,
		.data_source = direction,
		.iov = iov,
		.nr_segs = nr_segs,
		.iov_offset = 0,
		.count = count
	};
}
EXPORT_SYMBOL(iov_iter_init);

이름 그대로인 함수이다.

do_iter_read()

static ssize_t do_iter_read(struct file *file, struct iov_iter *iter,
		loff_t *pos, rwf_t flags)
{
	size_t tot_len;
	ssize_t ret = 0;

	if (!(file->f_mode & FMODE_READ))
		return -EBADF;
	if (!(file->f_mode & FMODE_CAN_READ))
		return -EINVAL;

	tot_len = iov_iter_count(iter);
	if (!tot_len)
		goto out;
	ret = rw_verify_area(READ, file, pos, tot_len);
	if (ret < 0)
		return ret;

	if (file->f_op->read_iter)
		ret = do_iter_readv_writev(file, iter, pos, READ, flags);
	else
		ret = do_loop_readv_writev(file, iter, pos, READ, flags);
out:
	if (ret >= 0)
		fsnotify_access(file);
	return ret;
}

만약 file->f_op->read_iter 함수가 등록되어 있었다면 do_iter_readv_writev()를 호출한다.
그렇지 않다면 do_loop_readv_writev()를 호출한다.

do_iter_readv_writev()

static ssize_t do_iter_readv_writev(struct file *filp, struct iov_iter *iter,
		loff_t *ppos, int type, rwf_t flags)
{
	struct kiocb kiocb;
	ssize_t ret;

	init_sync_kiocb(&kiocb, filp);
	ret = kiocb_set_rw_flags(&kiocb, flags);
	if (ret)
		return ret;
	kiocb.ki_pos = (ppos ? *ppos : 0);

	if (type == READ)
		ret = call_read_iter(filp, &kiocb, iter);
	else
		ret = call_write_iter(filp, &kiocb, iter);
	BUG_ON(ret == -EIOCBQUEUED);
	if (ppos)
		*ppos = kiocb.ki_pos;
	return ret;
}

먼저 kiocb를 초기화한다.
그 후 call_read_iter()를 통해 file->f_op->read_iter를 호출하게 된다.

do_loop_readv_writev()

static ssize_t do_loop_readv_writev(struct file *filp, struct iov_iter *iter,
		loff_t *ppos, int type, rwf_t flags)
{
	ssize_t ret = 0;

	if (flags & ~RWF_HIPRI)
		return -EOPNOTSUPP;

	while (iov_iter_count(iter)) {
		struct iovec iovec = iov_iter_iovec(iter);
		ssize_t nr;

		if (type == READ) {
			nr = filp->f_op->read(filp, iovec.iov_base,
					      iovec.iov_len, ppos);
		} else {
			nr = filp->f_op->write(filp, iovec.iov_base,
					       iovec.iov_len, ppos);
		}

		if (nr < 0) {
			if (!ret)
				ret = nr;
			break;
		}
		ret += nr;
		if (nr != iovec.iov_len)
			break;
		iov_iter_advance(iter, nr);
	}

	return ret;
}

file->f_op->read_iter가 없는 경우에 해당하므로 while 문을 돌면서 여러 번 filp->f_op->read를 호출한다.

Reference

profile
공부 내용 저장소

0개의 댓글

관련 채용 정보