如今的操作系统都是支持多任务、多用户的,计算机的资源是各个用户和任务共享的。操作系统通过setrlimit系统调用提供控制资源使用的方法。该函数的实现在各版本的内核里不尽相同,现在也支持了更多的能力,本文通过1.2.13的内核大致分析资源使用限制的一些原理。 首先在PCB中加了一个字段记录了限制信息。
struct rlimit rlim[RLIM_NLIMITS];
#define RLIMIT_CPU 0 /* CPU time in ms */
#define RLIMIT_FSIZE 1 /* Maximum filesize */
#define RLIMIT_DATA 2 /* max data size */
#define RLIMIT_STACK 3 /* max stack size */
#define RLIMIT_CORE 4 /* max core file size */
#define RLIMIT_RSS 5 /* max resident set size */
#define RLIMIT_NPROC 6 /* max number of processes */
#define RLIMIT_NOFILE 7 /* max number of open files *
rlimit的定义如下
struct rlimit {
// 当前的限制
long rlim_cur;
// 最大的限制,即调整值不能大于max
long rlim_max;
};
看完数据结构我们再看一下修改这个数据结构的函数。
asmlinkage int sys_setrlimit(unsigned int resource, struct rlimit *rlim)
{
struct rlimit new_rlim, *old_rlim;
int err;
if (resource >= RLIM_NLIMITS)
return -EINVAL;
err = verify_area(VERIFY_READ, rlim, sizeof(*rlim));
if (err)
return err;
memcpy_fromfs(&new_rlim, rlim, sizeof(*rlim));
old_rlim = current->rlim + resource;
// 超级用户可以随便修改阈值,非超级用户修改值的时候不能大于之前设置的最大值
if (((new_rlim.rlim_cur > old_rlim->rlim_max) ||
(new_rlim.rlim_max > old_rlim->rlim_max)) &&
!suser())
return -EPERM;
// RLIMIT_NOFILE代表进程能打开的文件大小,这个是操作系统本身的限制(NR_OPEN),无法突破
if (resource == RLIMIT_NOFILE) {
if (new_rlim.rlim_cur > NR_OPEN || new_rlim.rlim_max > NR_OPEN)
return -EPERM;
}
*old_rlim = new_rlim;
return 0;
}
看完资源限制的表示和设置方法,我们来看看各种限制的实现。
RLIMIT_CPU代表某个进程使用CPU的时间限制,包括用户态的时间和内核态的时间。当进程的CPU使用时间达到rlim_cur的值的时候,他会收到SIGXCPU信号,这个信号默认的处理是终止进程,但是用户可以设置处理该信号的处理函数,防止进程退出。这样进程可以继续执行,往后的每5秒该进程都会受到SIGXCPU 信号,直到CPU时间达到rlim_max的值,进程会收到SIGKILL信号。然后被终止。我们看看代码的实现。
if (
(current->rlim[RLIMIT_CPU].rlim_cur != RLIM_INFINITY) &&
(((current->stime + current->utime) % HZ) == 0)
) {
psecs = (current->stime + current->utime) / HZ;
// 达到软限制,发送信号
if (psecs == current->rlim[RLIMIT_CPU].rlim_cur)
send_sig(SIGXCPU, current, 1);
// 超过了软限制,每5秒再次发送SIGXCPU信号
else if ((psecs > current->rlim[RLIMIT_CPU].rlim_cur) &&
((psecs - current->rlim[RLIMIT_CPU].rlim_cur) % 5) == 0)
send_sig(SIGXCPU, current, 1);
}
// 达到硬限制,发送SIGKILL终止进程
if ((current->rlim[RLIMIT_CPU].rlim_max != RLIM_INFINITY) &&
(((current->stime + current->utime) / HZ) >= current->rlim[RLIMIT_CPU].rlim_max)
)
send_sig(SIGKILL, current, 1);
RLIMIT_FSIZE代表进程创建文件大小的限制。当进程创建文件的时候,会触发这个判断,如果达到了阈值。会返回-EFBIG错误码(文档注释说还会收到SIGXFSZ 信号,但是这个版本的内核没有实现)。
RLIMIT_DATA代表数据使用空间的限制,包括数据段,bss段和堆。因为数据段和bss段在编译的时候已经确认大小,只有堆可以修改大小。所以在修改堆大小的时候会触发这个校验。brk系统调用可以修改堆的大小。
RLIMIT_STACK代表栈的大小限制。比如我们在栈上访问了一个还没有映射到物理内存的虚拟地址,然后触发缺页中断,正常来说系统会映射一块物理内存到该虚拟地址,但是如果达到了阈值。则进程会收到SIGSEGV信号。
进程驻留内存的页数的大小限制
RLIMIT_NPROC代表当前进程所属的真实id对应的用户所能创建的最大进程数(线程)。触发校验的时机在fork。
this_user_tasks = 0;
free_task = -EAGAIN;
i = NR_TASKS;
// 遍历所有进程,找出uid和当前进程进程的uid一样的
while (--i > 0) {
if (!task[i]) {
free_task = i;
tasks_free++;
continue;
}
if (task[i]->uid == current->uid)
this_user_tasks++;
}
// 达到阈值
if (this_user_tasks > current->rlim[RLIMIT_NPROC].rlim_cur)
if (current->uid)
return -EAGAIN;
RLIMIT_NOFILE代表一个进程能打开文件个数的最大值。触发校验的时机是打开一个文件的时候。
int do_open(const char * filename,int flags,int mode)
{
struct inode * inode;
struct file * f;
int flag,error,fd;
// 找到一个可用的文件描述符,值小于NR_OPEN和RLIMIT_NOFILE(初始化的值大于NR_OPEN,表示用户没有设置过)。
for(fd=0; fd<NR_OPEN && fd<current->rlim[RLIMIT_NOFILE].rlim_cur; fd++)
// 还没被使用则找到可用的
if (!current->files->fd[fd])
break;
// 找不到可用的
if (fd>=NR_OPEN || fd>=current->rlim[RLIMIT_NOFILE].rlim_cur)
return -EMFILE;
...
}
这个版本的内核实现的限制控制不多,现代版本复杂了很多。今天就先分析到这。