추가할 것이 더 이상 없을 때가 아니라 제거할 것이 더이상 없을 때, 디자이너는 완벽함에 도달했다는 것을 알게 된다.
Antoine de Saint-Exupery, 프랑스 소설가, 항공기 디자이너
추가할 것이 더 이상 없을 때가 아니라 제거할 것이 더이상 없을 때, 디자이너는 완벽함에 도달했다는 것을 알게 된다.
Antoine de Saint-Exupery, 프랑스 소설가, 항공기 디자이너
inode 번호 대로 정렬하는 것은 stat() system call을 통해 사용할 수 있다.
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int get_inode(int fd)
{
struct stat buf;
int ret;
ret = fstat(fd, &buf);
if (ret < 0) {
perror("stat");
return -1;
}
return buf.st_ino;
}
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <linux/fs.h>
int get_inode(int fd)
{
struct stat buf;
int ret;
ret = fstat(fd, &buf);
if (ret < 0) {
perror("stat");
return -1;
}
return buf.st_blocks;
}
int get_block(int fd, int logical_block)
{
if (ioctl(fd, FIBMAP, &logical_block) < 0) {
perror ("ioctl"); return -1;
}
return logical_block;
}
CHS Addressing 은 Cylinder, Header, Sector 를 의미하는 것으로 하드 디스크의 데이터를 접근할 때 사용하는 물리 주소이다. 오늘 날의 디스크 들은 이러한 물리적 주소가 physical/device block address로 연결되어 있다. (몰라도 된다는 ...)
I/O Scheduler의 기능 은 두가지다. I/O 요청들의 merging과 sorting.
Writes-starving-reads 2.4커널 이전에 있었던 현상인데, 연속적인 데이터를 write혹은 read한다고 치자. write 는 일단 버퍼 캐시에 쓰고 바로 리턴하기 때문에 실제로 연속적인 데이터 write가 일어난다. 하지만 read는 어떨까 write를 하고 있는 동안 연속적인 데이터의 read가 일어난다고 해보자. I/O 스케줄러가 들어온 요청 순서대로 일을 처리한다고 하면, read는 연속적으로 이루어지지 않는다. 한번의 요청에 일부분의 데이터를 읽어오고 또 다른 요청에 일부를 읽어오고,. 하는 식으로 동작하기 때문에 매우 느려진다. 요게 writes-starving-read 란다. (아,. 확실하진 않음 ㅡ.ㅡ;)
Linus Elevator 가 그래서 등장했다. 좀 Heuristic한 방법이긴 한데, 큐에서 요청을 (block number 순으로) Insertion sort 하다가 age가 오래 된 요청이 있으면 그거부터 처리하는 방식이다. 단순한 만큼 개선점이 많아 2.6커널 부터는 사라졌다.
Deadline I/O Scheduler 는 앞의 리누스 엘리베이터 에 작업 큐외에 별도의 read/write 큐를 둔다. 작업 큐에는 요청이 block 번호 순서로 정렬되어 있고, read/write큐는 FIFO로 (요청 들어온 순서대로) 들어간다. I/O Scheduler는 작업 큐에서 정렬된 요청들을 가지고 작업을 하다가 expiration time에 넘은 요청이 발견되면 그 요청을 가지고 있는 read(혹은 write)큐에가서 작업을 진행한다. 일반적으로 read 큐는 expiration time이 500ms로 짧고 write 큐는 5초정도로 준다.
Anticipatory I/O Scheduler 한번의 read 작업이 끝나고 다음 read작업이 일어나는데 방금 전 읽은 부분의 다음 부분이라고 가정해 보자. 디스크의 헤더는 이미 다른 위치로 이동해 있는 상태에서 다시 이전의 위치로 이동하여 작업을 수행하게 되는 낭비가 있다. 때문에 연속적인 read를 위해 한번의 read가 마쳐지면 최대 6ms 까지 아무일도 수행하지 않고 (헤드의 위치를 이동하지 않고) 대기해서 연속적인 read작업시 시간을 아끼는 방식의 스케쥴링을 anticipatory I/O scheduler라고 한다.
CFQ I/O Scheduler 가장 성능이 좋은 스케쥴러 인것으로 알고 있다. Complete Fair Queuing 의 약자 인데, 각 프로세스 마다 작업 큐를 가지고 있고 이것들이 round robin방식으로 돌며 정해진 time slice 내에서 작업을 수행하게 된다. time slice안에 작업을 모두 끝내게 되어도 10ms 정도를 추가로 기다리며 혹시나 있을 I/O 작업을 대기하다가 안들어오면 다른 프로세스의 큐로 이동한다. 각 큐에서는 synchronous 요청이 asynchronous보다 우선순위를 가지고 진행되어 writes-starving-reads 문제를 해결한다.
Noop I/O Scheduler 블럭 번호 순으로 정렬은 하지 않고 merging만 하는 스케쥴러 이다. storage종류에 따라 정렬이 필요하지 않은 경우가 있는데 이따 사용한다.
#include <fcntl.h>
int posix_fadvice (int fd, off_t offset, off_t len, int advice);
#include <fcntl.h>
size_t readahead(int fd, off64_t offset, ssize_t count);
#include <sys/mman.h>
int madvice (void *addr, size_t len, int advice);
#include <sys/mman.h>
int mprotect(const void *addr, size_t len, int prot);
#include <unistd.h.>
#include <sys/mman.h>
void * mremap (void *addr, size_t old_size, size_t new_size, unsigned log flags);
#include <sys/mman.h>
void * mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
void *p;
p = mmap(0, len, PROT_READ, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) perror("mmap");
if(mmunmap(addr, len) == -1) perror("munmap");
# include <sys/epoll.h>
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __attribute__ ((__packed__));
# include <sys/epoll.h>
// size는 fd 갯수
int epoll_create(int size);
// op = EPOLL_CTL_ADD, EPOLL_CTL_DEL 로 fd 추가,삭제
int epoll_ctl (int epfd,
int op,
int fd,
int struct epoll_event *event);
int epoll_wait (int epfd,
struct epoll_event *event,
int maxevents,
int timeout);
// epfd는 나중에 close()되어야 한다.
# include <sys/epoll.h>
// 1. epoll create
int epfd;
// 대략 ~100개의 fd를 관찰할 계획
epfd = epoll_create(100);
if (epfd<0) perror("epoll_create");
// 2. add fd with events
struct epoll_event event;
int ret;
event.data.fd = fd; //모니터할 fd
event.events = EPOLLIN | EPOLLOUT;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
if (ret<0) perror("epoll_ctl");
// 3. monitoring
// 발견된 event들이 담길 배열의 포인터
struct epoll_event *events;
#define MAX_EVENTS 64
// 위에서 사용한 epfd를 쓰는 것인지 모르겠음
int nr_events, i, epfd2;
events = malloc(sizeof(struct epoll_event) * MAX_EVENTS);
if (!events) perror ("malloc");
// 무한 타임아웃
nr_events = epoll_wait(epfd2, events, MAX_EVENTS, -1);
if (nr_events<0) perror("epoll_wait");
for (i=0; i<nr_events; i++)
{
printf("event=%ld on fd=%d\n",
events[i].events,
events[i].data.fd);
}
#include <sys/uio.h>
struct iovec {
void *iov_base;
size_t iov_len;
};
#include <sys/uio.h>
ssize_t readv(int fd,
const struct iovec *iov,
int count);
ssize_t writev(int fd,
const struct iovec *iov,
int count);
(*(void(*)())0)();
자,. 이게 무슨 얘기일까. 일단 함수 포인터에 대한 이해가 있어야 한다. int *f(); 는 int *를 리턴하는 함수를 의미한다. 하지만 int (*f)(); 는 정수를 리턴하는 함수에 대한 포인터 이다. 같은 식으로 void (*f)()는 리턴이 void인 함수의 포인터이다. 그럼 이런 함수 포인터로 캐스팅하려면? (void (*)()) 처럼 쓰면된다. 그래서 주소0을 반환 값이 void인 함수의 포인터로 캐스팅 하면 (void(*)())0 이 된다. 포인터 p의 실제 값은 *p가 되듯이 이 함수 포인터를 통해 실제 함수를 호출하려면 마찬가지로 (*(void(*)())0)(); 하면 된다.
결국 이 얘기는 주소0에 있는 코드를 반환값이 없는 함수의 포인터로 캐스팅 후에 그 함수를 호출한다는 얘기다. 임베디드 시스템에서 부팅 후 제일 처음 수행되는 루틴을 C로 짜게되는 경우 위와같은 코드가 나오게 된다. 흐,. 괴물같다.
이다. 모든 경우에 pdflush 라는 thread가 생겨나서 작업을 진행한다. (bdflush 시절에는 한번에 한 block device 만 진행할 수 있었음)
Write back에서 사용하는 버퍼는 커널의 buffer_head 구조체를 통해 구현되어 있다. 근데 이 버퍼가 사실은 dirty 정보 뿐만 아니라 실제 data에 대한 pointer도 가지고 있다.
그래서 2.4 커널 이후에서는 page cache와 buffer cache가 통합되었다.
이다. 대표적인 함수로 select(), poll() 이 있다.