커널 모듈인 ufs는 화일 시스템에 쓰기 행위가 발생할 때 disk quota를 확인하는 과정을 밟습니다. 이러한 과정은 quotaon/off와는 관계없이 실행되는데, quota on이 되어 있으면 많은 것을 실행하는 것이고, quotaoff인 경우에는 빠르게 리턴하는 것이죠.
다음은 ufs_write시 일어나는 ufs 모듈내의 흐름입니다. 더 하부 모듈인 genunix, unix와 관련된 다른 모듈은 캡쳐하지 않았습니다. dtrace로 캡쳐했으며 캡쳐한 방법은 다음과 같습니다.
#dtrace -F -n 'io:::start/uid==31523 && args[1]->dev_statname =="cmdk0"/{trace(timestamp);self->trace=1;}' \
-n 'fbt:ufs:ufs_write:entry/self->trace/{trace(timestamp);self->ufs=1;}' \
-n 'fbt:ufs::/self->trace&&self->ufs/{trace(timestamp);}' \
-n 'fbt:ufs:ufs_write:return/self->trace&&self->ufs/ {trace(timestamp);self->trace=0;self->ufs=0;exit(1);}'
결과는 아래 그림과 같다.
그림에서 보듯이, ufs_write가 발생할때 chkdq라는 것을 지나가는데, chkdq()는 아래와 같은 내용을 가진다.
ufs quota는 inode quota와 data quota를 동시에 가지는데, 아래는 ufs_write()를 통해서 data block의 write가 일어난 경우를 잡은 경우이기 때문에 data quota만 체크한다.
그런데, 중요한것은 chkdq()에서 무엇을 하느냐 인데, 아래 코드에서 볼 수 있는 바와 같이,
다음은 ufs_write시 일어나는 ufs 모듈내의 흐름입니다. 더 하부 모듈인 genunix, unix와 관련된 다른 모듈은 캡쳐하지 않았습니다. dtrace로 캡쳐했으며 캡쳐한 방법은 다음과 같습니다.
#dtrace -F -n 'io:::start/uid==31523 && args[1]->dev_statname =="cmdk0"/{trace(timestamp);self->trace=1;}' \
-n 'fbt:ufs:ufs_write:entry/self->trace/{trace(timestamp);self->ufs=1;}' \
-n 'fbt:ufs::/self->trace&&self->ufs/{trace(timestamp);}' \
-n 'fbt:ufs:ufs_write:return/self->trace&&self->ufs/ {trace(timestamp);self->trace=0;self->ufs=0;exit(1);}'
결과는 아래 그림과 같다.
그림에서 보듯이, ufs_write가 발생할때 chkdq라는 것을 지나가는데, chkdq()는 아래와 같은 내용을 가진다.
ufs quota는 inode quota와 data quota를 동시에 가지는데, 아래는 ufs_write()를 통해서 data block의 write가 일어난 경우를 잡은 경우이기 때문에 data quota만 체크한다.
그런데, 중요한것은 chkdq()에서 무엇을 하느냐 인데, 아래 코드에서 볼 수 있는 바와 같이,
<br /><br /><span style="font-family: courier new;font-size:85%;" >124 int<br />125 chkdq(struct inode *ip, long change, int force, struct cred *cr,<br />126 char **uerrp, size_t *lenp)<br />127 {<br />128 struct dquot *dqp;<br />129 uint64_t ncurblocks;<br />130 struct ufsvfs *ufsvfsp = ip->i_ufsvfs;<br />131 int error = 0;<br />132 long abs_change;<br />133 char *msg1 =<br />134 "!quota_ufs: over hard disk limit (pid %d, uid %d, inum %d, fs %s)\n";<br />135 char *msg2 =<br />136 "!quota_ufs: Warning: over disk limit (pid %d, uid %d, inum %d, fs %s)\n";<br />137 char *msg3 =<br />138 "!quota_ufs: over disk and time limit (pid %d, uid %d, inum %d, fs %s)\n";<br />139 char *msg4 =<br />140 "!quota_ufs: Warning: quota overflow (pid %d, uid %d, inum %d, fs %s)\n";<br />141 char *errmsg = NULL;<br />142 time_t now;<br />143<br />144 /*<br />145 * Shadow inodes do not need to hold the vfs_dqrwlock lock.<br />146 */<br />147 ASSERT((ip->i_mode & IFMT) == IFSHAD ||<br />148 RW_LOCK_HELD(&ufsvfsp->vfs_dqrwlock));<br />149 ASSERT(RW_WRITE_HELD(&ip->i_contents));<br />150<br />151 if (change == 0)<br />152 return (0);<br />153 dqp = ip->i_dquot;<br />154<br />155 /*<br />156 * Make sure the quota info record matches the owner.<br />157 */<br />158 ASSERT(dqp == NULL || ip->i_uid == dqp->dq_uid);<br />159<br />160 #ifdef DEBUG<br />161 /*<br />162 * Shadow inodes and extended attribute directories<br />163 * should not have quota info records.<br />164 */<br />165 if ((ip->i_mode & IFMT) == IFSHAD || (ip->i_mode & IFMT) == IFATTRDIR) {<br />166 ASSERT(dqp == NULL);<br />167 }<br />168 /*<br />169 * Paranoia for verifying that quotas are okay.<br />170 */<br />171 else {<br />172 struct dquot *expect_dq;<br />173 int mismatch_ok = 0;<br />174<br />175 /* Get current quota information */<br />176 expect_dq = getinoquota(ip);<br />177 /*<br />178 * We got NULL back from getinoquota(), but there is<br />179 * no error code return from that interface and some<br />180 * errors are "ok" because we may be testing via error<br />181 * injection. If this is not the quota inode then we<br />182 * use getdiskquota() to see if there is an error and<br />183 * if the error is ok.<br />184 */<br />185 if (expect_dq == NULL && ip != ufsvfsp->vfs_qinod) {<br />186 int error;<br />187 struct dquot *xdqp;<br />188<br />189 error = getdiskquota((uid_t)ip->i_uid, ufsvfsp, 0,<br />190 &xdqp);<br />191 switch (error) {<br />192 /*<br />193 * Either the error was transient or the quota<br />194 * info record has no limits which gets optimized<br />195 * out by getinoquota().<br />196 */<br />197 case 0:<br />198 if (xdqp->dq_fhardlimit == 0 &&<br />199 xdqp->dq_fsoftlimit == 0 &&<br />200 xdqp->dq_bhardlimit == 0 &&<br />201 xdqp->dq_bsoftlimit == 0) {<br />202 mutex_enter(&xdqp->dq_lock);<br />203 dqput(xdqp);<br />204 mutex_exit(&xdqp->dq_lock);<br />205 } else {<br />206 expect_dq = xdqp;<br />207 }<br />208 break;<br />209<br />210 case ESRCH: /* quotas are not enabled */<br />211 case EINVAL: /* error flag set on cached record */<br />212 case EUSERS: /* quota table is full */<br />213 case EIO: /* I/O error */<br />214 mismatch_ok = 1;<br />215 break;<br />216 }<br />217 }<br />218<br />219 /*<br />220 * Make sure dqp and the current quota info agree.<br />221 * The first part of the #ifndef is the quick way to<br />222 * do the check and should be part of the standard<br />223 * DEBUG code. The #else part is useful if you are<br />224 * actually chasing an inconsistency and don't want<br />225 * to have to look at stack frames to figure which<br />226 * variable has what value.<br />227 */<br />228 #ifndef CHASE_QUOTA<br />229 ASSERT(mismatch_ok || dqp == expect_dq);<br />230 #else /* CHASE_QUOTA */<br />231 if (expect_dq == NULL) {<br />232 /*<br />233 * If you hit this ASSERT() you know that quota<br />234 * subsystem does not expect quota info for this<br />235 * inode, but the inode has it.<br />236 */<br />237 ASSERT(mismatch_ok || dqp == NULL);<br />238 } else {<br />239 /*<br />240 * If you hit this ASSERT() you know that quota<br />241 * subsystem expects quota info for this inode,<br />242 * but the inode does not have it.<br />243 */<br />244 ASSERT(dqp);<br />245 /*<br />246 * If you hit this ASSERT() you know that quota<br />247 * subsystem expects quota info for this inode<br />248 * and the inode has quota info, but the two<br />249 * quota info pointers are not the same.<br />250 */<br />251 ASSERT(dqp == expect_dq);<br />252 }<br />253 #endif /* !CHASE_QUOTA */<br />254 /*<br />255 * Release for getinoquota() above or getdiskquota()<br />256 * call when error is transient.<br />257 */<br />258 if (expect_dq) {<br />259 mutex_enter(&expect_dq->dq_lock);<br />260 dqput(expect_dq);<br />261 mutex_exit(&expect_dq->dq_lock);<br />262 }<br />263 }<br />264 #endif /* DEBUG */<br />265<br />266 /*<br />267 * Shadow inodes and extended attribute directories<br />268 * do not have quota info records.<br />269 */<br />270 if (dqp == NULL)<br />271 return (0);<br />272 /*<br />273 * Quotas are not enabled on this file system so there is nothing<br />274 * more to do.<br />275 */<br />276 if ((ufsvfsp->vfs_qflags & MQ_ENABLED) == 0) {<br />277 return (0);<br />278 }<br />279 mutex_enter(&dqp->dq_lock);<br />280 if (change <>dq_flags |= DQ_MOD;<br />282 abs_change = -change; /* abs_change must be positive */<br />283 if (dqp->dq_curblocks <>dq_curblocks = 0;<br />285 else<br />286 dqp->dq_curblocks += change;<br />287 if (dqp->dq_curblocks <>dq_bsoftlimit)<br />288 dqp->dq_btimelimit = 0;<br />289 dqp->dq_flags &= ~DQ_BLKS;<br />290 TRANS_QUOTA(dqp);<br />291 mutex_exit(&dqp->dq_lock);<br />292 return (0);<br />293 }<br />294<br />295 /*<br />296 * Adding 'change' to dq_curblocks could cause an overflow.<br />297 * So store the result in a 64-bit variable and check for<br />298 * overflow below.<br />299 */<br />300 ncurblocks = (uint64_t)dqp->dq_curblocks + change;<br />301<br />302 /*<br />303 * Allocation. Check hard and soft limits.<br />304 * Skip checks for uid 0 owned files.<br />305 * This check used to require both euid and ip->i_uid<br />306 * to be 0; but there are no quotas for uid 0 so<br />307 * it really doesn't matter who is writing to the<br />308 * root owned file. And even root cannot write<br />309 * past a user's quota limit.<br />310 */<br />311 if (ip->i_uid == 0)<br />312 goto out;<br />313<br />314 /*<br />315 * Disallow allocation if it would bring the current usage over<br />316 * the hard limit or if the user is over his soft limit and his time<br />317 * has run out.<br />318 */<br />319 if (dqp->dq_bhardlimit && ncurblocks >= (uint64_t)dqp->dq_bhardlimit &&<br />320 !force) {<br />321 /* If the user was not informed yet and the caller */<br />322 /* is the owner of the file */<br />323 if ((dqp->dq_flags & DQ_BLKS) == 0 &&<br />324 ip->i_uid == crgetruid(cr)) {<br />325 errmsg = msg1;<br />326 dqp->dq_flags |= DQ_BLKS;<br />327 }<br />328 error = EDQUOT;<br />329 goto out;<br />330 }<br />331 if (dqp->dq_bsoftlimit && ncurblocks >= (uint64_t)dqp->dq_bsoftlimit) {<br />332 now = gethrestime_sec();<br />333 if (dqp->dq_curblocks <>dq_bsoftlimit ||<br />334 dqp->dq_btimelimit == 0) {<br />335 dqp->dq_flags |= DQ_MOD;<br />336 dqp->dq_btimelimit = now +<br />337 ((struct ufsvfs *)ITOV(ip)->v_vfsp->vfs_data)<br />338 ->vfs_btimelimit;<br />339 if (ip->i_uid == crgetruid(cr)) {<br />340 errmsg = msg2;<br />341 }<br />342 } else if (now > dqp->dq_btimelimit && !force) {<br />343 /* If the user was not informed yet and the */<br />344 /* caller is the owner of the file */<br />345 if ((dqp->dq_flags & DQ_BLKS) == 0 &&<br />346 ip->i_uid == crgetruid(cr)) {<br />347 errmsg = msg3;<br />348 dqp->dq_flags |= DQ_BLKS;<br />349 }<br />350 error = EDQUOT;<br />351 }<br />352 }<br />353 out:<br />354 if (error == 0) {<br />355 dqp->dq_flags |= DQ_MOD;<br />356 /*<br />357 * ncurblocks can be bigger than the maximum<br />358 * number that can be represented in 32-bits.<br />359 * When copying ncurblocks to dq_curblocks<br />360 * (an unsigned 32-bit quantity), make sure there<br />361 * is no overflow. The only way this can happen<br />362 * is if "force" is set. Otherwise, this allocation<br />363 * would have exceeded the hard limit check above<br />364 * (since the hard limit is a 32-bit quantity).<br />365 */<br />366 if (ncurblocks > 0xffffffffLL) {<br />367 dqp->dq_curblocks = 0xffffffff;<br />368 errmsg = msg4;<br />369 } else {<br />370 dqp->dq_curblocks = ncurblocks;<br />371 }<br />372 }<br />373<br />374 if (dqp->dq_flags & DQ_MOD)<br />375 TRANS_QUOTA(dqp);<br />376<br />377 mutex_exit(&dqp->dq_lock);<br />378 /*<br />379 * Check for any error messages to be sent<br />380 */<br />381 if (errmsg != NULL) {<br />382 /*<br />383 * Send message to the error log.<br />384 */<br />385 if (uerrp != NULL) {<br />386 /*<br />387 * Set up message caller should send to user;<br />388 * gets copied to the message buffer as a side-<br />389 * effect of the caller's uprintf().<br />390 */<br />391 *lenp = strlen(errmsg) + 20 + 20 +<br />392 strlen(ip->i_fs->fs_fsmnt) + 1;<br />393 *uerrp = (char *)kmem_alloc(*lenp, KM_NOSLEEP);<br />394 if (*uerrp != NULL) {<br />395 /* errmsg+1 => skip leading ! */<br />396 (void) sprintf(*uerrp, errmsg+1,<br />397 (int)ttoproc(curthread)->p_pid,<br />398 (int)ip->i_uid, (int)ip->i_number,<br />399 ip->i_fs->fs_fsmnt);<br />400 }<br />401 } else {<br />402 /*<br />403 * Caller doesn't care, so just copy to the<br />404 * message buffer.<br />405 */<br />406 cmn_err(CE_NOTE, errmsg,<br />407 (int)ttoproc(curthread)->p_pid,<br />408 (int)ip->i_uid, (int)ip->i_number,<br />409 ip->i_fs->fs_fsmnt);<br />410 }<br />411 }<br />412 return (error);<br />413 }<br /></span></pre><span style="font-family: courier new;font-size:100%;" >흥미로운 것은 솔라리스 10에 새로이 생긴 zfs에서는 어떻게 quota를 사용할까 ?<br />zfs는 metadata에 대한 quota를 사용하지 않는다, 기본적으로 화일의 수를 할당된 공간과 별도로 체크하는 것이 의미가 없기 때문이다. 따라서, 또한, zfs는 dataset layer에서 블럭을 할당할때마다 used byte가 할당이 되며, 모든 블럭은 객체지향적으로 상속되므로, uber block은 하위 블럭의 총합에 대한 것을 늘 유지하게 되므로 quota check하는 것이 매우 단순하다.<br /><br /></span><pre><span style="font-family: courier new;font-size:85%;" ><span style="font-size:100%;">아래가 dataset에서 quota를 check하는 모듈이다. 매우 단순함을 알 수 있다.<br />코드의 길이만 봐도 ufs의 경우에는 300라인에 해당하지만, zfs의 경우에는<br />40여라인밖에 되지 않는다. 아래의 모듈에서는 위 ufs용 모듈인 chkdq()와는 달리<br />사용자 id를 기준으로 비교하는 라인도 없다.<br /><br />코드의 길이가 중요한 이유는 data를 쓰기하는 경우에, 솔라리스는 mutex_lock()을 설정하는데<br />코드의 길이가 길게 되며, lock이 잡히는 시간이 증가하게 되므로, 병목의 발생 가능성이 증가하게<br />된다.<br /><br />따라서, zfs는 quota() 기능 사용에 따른 오버헤드가 매우 작을 수 있음을 예측할 수 있다.<br />반면, 이런 오해가 있을 수도 있다. ufs는 단일 화일 시스템을 여러 사용자를 기준으로 사용 공간을<br />나누어 줄 수 있는데, zfs는 불가능한 것이 아닌가?<br /><br />그러나, 이런 오해는 ufs와 zfs의 근본적인 차이를 이해하지 못해서 발생하는 것이다. zfs는 모든 것이<br />pool에 기반하기 때문에 사용자별로 별개의 zfs를 만들어서 할당할 수 있는 볼륨기능이 통합되어 있다.<br />따라서, 단일 화일시스템을 사용자별로 쪼개고 붙이고할 필요가 없다. 그냥 필요한 공간만큼 할당만<br />하면 된다. 혹여나 모 사용자에게 너무 많이 할당했다면, 반납하고 필요한 사용자에게 할당하면 된다.<br /><br />반면, ufs는 볼륨 매니저에서 사용자별로 쪼개놓지 않으면 하나의 단일 화일 시스템에서 여러 사용자용<br />쿼타를 설정해야하므로 매우 불편할 뿐 아니라, 다른 화일 시스템의 사용자에게 남는 공간을 떼어줄 수도<br />없다.<br /><br />어떤 화일 시스템을 사용해야 하는가? 관리의 포인트를 중히 여긴다면 선택의 여지가 없다. 두말할 필요없이<br />zfs라고 할 수 있겠다. zfs 기반으로 서비스를 제공하는 순간 많은 사람들이 편하게 된다는 점을 염두에<br />두어야 한다. 서비스 업체들은 자사 엔지니어가 스토리지 관리때문에 고객들에게 뺏기는 시간이 혁신적으로<br />짧아진다는 것을 알아야한다. 왜냐하면, 시간은 곧 돈이기 때문이다.</span><br /><br /><br /><br />2736 int<br />2737 dsl_dataset_check_quota(dsl_dataset_t *ds, boolean_t check_quota,<br />2738 uint64_t asize, uint64_t inflight, uint64_t *used, uint64_t *ref_rsrv)<br />2739 {<br />2740 int error = 0;<br />2741<br />2742 ASSERT3S(asize, >, 0);<br />2743<br />2744 /*<br />2745 * *ref_rsrv is the portion of asize that will come from any<br />2746 * unconsumed refreservation space.<br />2747 */<br />2748 *ref_rsrv = 0;<br />2749<br />2750 mutex_enter(&ds->ds_lock);<br />2751 /*<br />2752 * Make a space adjustment for reserved bytes.<br />2753 */<br />2754 if (ds->ds_reserved > ds->ds_phys->ds_unique_bytes) {<br />2755 ASSERT3U(*used, >=,<br />2756 ds->ds_reserved - ds->ds_phys->ds_unique_bytes);<br />2757 *used -= (ds->ds_reserved - ds->ds_phys->ds_unique_bytes);<br />2758 *ref_rsrv =<br />2759 asize - MIN(asize, parent_delta(ds, asize + inflight));<br />2760 }<br />2761<br />2762 if (!check_quota || ds->ds_quota == 0) {<br />2763 mutex_exit(&ds->ds_lock);<br />2764 return (0);<br />2765 }<br />2766 /*<br />2767 * If they are requesting more space, and our current estimate<br />2768 * is over quota, they get to try again unless the actual<br />2769 * on-disk is over quota and there are no pending changes (which<br />2770 * may free up space for us).<br />2771 */<br />2772 if (ds->ds_phys->ds_used_bytes + inflight >= ds->ds_quota) {<br />2773 if (inflight > 0 || ds->ds_phys->ds_used_bytes <>ds_quota)<br />2774 error = ERESTART;<br />2775 else<br />2776 error = EDQUOT;<br />2777 }<br />2778 mutex_exit(&ds->ds_lock);<br />2779<br />2780 return (error);<br />2781 }</span><br />