进程对象
进程的特权
进程的特权基于用户ID,所以更改进程对象中用户ID就像更改进程的特权,如下是若干个可以更改进程对象用户ID的接口:
setuid(uid)
if(进程具有超级用户特权)/* 是指有效用户ID==0 */
实际用户 ID=uid;
有效用户 ID=uid;
保存的设置用户 ID=uid;
else if(uid==实际用户 ID || uid==保存的设置用户 ID)
有效用户 ID=uid;
else return -1;
setgid(gid)
if(进程具有超级用户特权)/* 是指有效用户ID==0 */
实际组 ID=gid;
有效组 ID=gid;
保存的设置组 ID=gid;
else if(gid==实际组 ID || gid==保存的设置组 ID)
有效组 ID=gid;
else return -1;
setreuid(ruid,euid)
if(ruid == 实际用户ID || ruid == 有效用户ID)
实际用户ID = ruid;
if(euid == 实际用户ID || euid == 有效用户ID || euid==保存的设置用户ID)
有效用户ID = euid;
保存的设置用户ID = 有效用户ID;
/* If the real user ID is set or the effective user ID is set to a value not equal to the previous real user ID, the saved set-user-ID will be set to the new effective user ID.
TODO,如何理解这句话, */
-
我对这句话(上面注释里买你的英语)的理解就是"当实际用户ID或有效用户ID更改为一个不同于原有的实际用户ID的值时,保存的设置用户ID会设置为新的有效用户ID",按照这个理论也就是说,
生成一个程序,所有者为root;设置 SUID 标志,然后以普通用户(如 ww)启动,此时进程的三个ID应该是:ruid=ww;euid=root;保存的设置用户ID=root
此时调用 setreuid(getuid(),getuid());此时:ruid=ww;euid=ww;保存的设置用户ID=root;[因为此时 euid 的新值与原来的 ruid 一致,所以不更改保存的设置用户ID]
再调用 setreuid(getuid(),root);此时 root== 保存的设置用户ID,所以理论上应该会将有效用户ID设置为 root;但是测试发现此时提示 Operation not permitted,而且有效用户ID没变,猜测应该是第2步将保存的设置用户ID修改为了有效用户ID的新值(即ww),who know;
ww$ cat main.c
#include <unistd.h>
#include <stdio.h>
int main(int argc,char *argv[]){
int ruid = getuid();
int euid = geteuid();
printf("ruid:%d;euid:%d\n",getuid(),geteuid());
if( setreuid(ruid,ruid)<0 )
printf("%m\n");
printf("ruid:%d;euid:%d\n",getuid(),geteuid());
if( setreuid(ruid,euid)<0 )
printf("%m\n");
printf("ruid:%d;euid:%d\n",getuid(),geteuid());
return 0;
}
ww$ gcc main.c -o test
ww$ su
密码:
root# chown root:root test
root# chmod +s test
root# ls -l test
-rwsrwsr-x 1 root root 8671 2月 4 00:03 test
root# exit
ww$ ./test
ruid:1000;euid:0
ruid:1000;euid:1000
Operation not permitted
ruid:1000;euid:1000
setregid(),同 setreuid(),只是将用户ID更改为组ID.
seteuid(uid)
if(进程具有超级用户特权)/* 是指有效用户ID==0 */
有效用户 ID=uid;
else if(uid==实际用户 ID || uid==保存的设置用户 ID)
有效用户 ID=uid;
else return -1;
setegid(),同 seteuid(),
umask
文件创建模式屏蔽字,在创建文件(包括任何类型的文件)时起作用.如:在调用 open(),mkdir() 创建文件时,文件的实际权限 = mode(参数指定的权限) & ~umask.
/* 此时新建目录的权限应该是 rwxrwxrwx(0777),但是由于 umask=0002,
所以目录的实际权限: 0777 & ~0002 = 111111101b,即 rwxrwxr-x. */
safe_mkdir(argv[1],0777);
当前工作目录
在进程对象中,并没有保存当前工作目录的完整路径名,而只是存放着一个指针,该指针指向着当前工作目录对应的 v-node(参考Unix环境高级编程第三章,打开文件的数据结构).
int origin_cwd_fd = safe_open(".",O_RDONLY);/* 保存当前工作目录,即不使用 getcwd() 来保存当前工作目录 */
safe_chdir("..");/* 切换当前工作目录 */
safe_fchdir(origin_cwd_fd);/* 恢复当前工作目录 */
附加组ID
附加组 ID,是一个 gid_t 类型的数组,主要用于测试进程对文件是否具有访问的权限.
通过 getgroups()/setgroups() 来设置与获取进程组 ID,具体函数说明如下:
/**
* 获取进程当前附加组 ID.
* @param size 若为0,则返回附加组 ID 的数目,进程可以先获取附加组 ID 的数目,然后在复制足够的空间.
*/
int getgroups(int size, gid_t *list);
/** 将 list 指向的长度为 size 的数组设置为进程的附加组 ID,内核不会检测附加组 ID 的有效性.如
@code
gid_t group_list[]={1,2,3,4,5};//内核不会检测是否存在 ID 为 1 的用户组.
setgroups(sizeof(group_list)/sizeof(gid_t),group_list);
@endcode
*/
int setgroups(size_t size, const gid_t *list);
资源限制
每个进程都有一组资源限制,用于限制该进程对资源的使用,防止过度使用,相关数据结构与接口如下:
struct rlimit {
rlim_t rlim_cur;/* 资源的的当前限制值,即进程对资源的使用不能超过该值 */
rlim_t rlim_max;/* rlim_cur 的最大值, */
};
/* 若 rlim_cur,rlim_max 值为 RLIM_INFINITY,则表示无限制 */
int getrlimit(int resource, struct rlimit *rlim);/* 获取对进程在资源 resource 的限制,并存放在 rlim 指向的结构体中. */
int setrlimit(int resource, const struct rlimit *rlim);
int main(int argc,char *argv[]){
struct rlimit res_limit;
getrlimit(RLIMIT_CPU,&res_limit);/* 获取 CPU 使用时间的资源限制 */
ByteArray str("cpu: ");
print_rlimit(&res_limit,str);
puts(str.constData());
res_limit.rlim_cur = 1;
setrlimit(RLIMIT_CPU,&res_limit);/* 将对 CPU 使用时间的资源限制设置为 1 s. */
for(;;);/* 执行 1s 后将收到信号 SIGXCPU */
return 0;
}
// 执行输出:
$./Test
cpu: rlim_cur: 无限制,rlim_max: 无限制
超出 CPU 时限 (核心已转储)
剩余闹钟时间
uint alarm(uint seconds);将剩余闹钟时间设置为 seconds,并返回之前的剩余闹钟时间,当 seconds 大于0时,剩余闹钟事件会每隔1s自减1直至为0,当剩余闹钟时间减至0时,会发送 SIGALRM 信号到当前进程.
屏蔽信号集
屏蔽信号集,参见"Unix环境高级编程-9-信号"记录了其详细用途.
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);设置屏蔽信号集,或者获取当前屏蔽信号集,流程如下:
如果 oldset 不为0,则将当前屏蔽信号集复制到 oldset 指向的信号集中.
如果 set 不为0,则按照 how 的值来设置屏蔽信号集,how 可取:
SIG_BLOCK,此时新的屏蔽信号集为原有的屏蔽信号集与 set 的并集.
SIG_UNBLOCK,此时新的屏蔽信号集为原有的屏蔽信号集与 set 补集的交集,即从原有的屏蔽信号集中移除在 set 中包含的信号
SIG_SETMASK,此时将 set 设置为新的屏蔽信号集.
未决信号集
未决信号集,存放着已经产生,但是由于被进程阻塞而未递送给进程的信号.
int sigpending(sigset_t *set);将当前线程与当前进程未决信号集的并集存入 set 中.
信号处理方式
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);用来获取当前的信号处理方式,或者设置新的信号处理方式,流程如下:
如果 oldact 不为0,则将当前对信号 signum 的处理方式复制到 oldact 指向的缓冲区中.
如果 act 不为0,则将 act 设置为信号 signum 新的处理方式.
struct sigaction
struct sigaction,具体地描述了信号处理方式,具体结构:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
};
sa_handlder,可以是信号处理函数的指针,或者是 SIG_DFL,SIG_IGN.
sa_sigaction,只能是信号处理函数的指针,当 sa_flags 中设置了 SA_SIGINFO 标志时,调用 sa_sigaction 指向的信号处理函数;否则调用 sa_handler 指向的信号处理函数.
sa_mask,在调用信号处理函数之前,会将 sa_mask 添加到进程的屏蔽信号集中(SIG_BLOCK),并在调用信号处理函数之后,恢复进程的屏蔽信号集.
sa_flags,位掩码,常见的标志如下:
SA_RESTART,自动重启被中断的系统调用,如下:
int main(int argc,char *argv[]){
struct sigaction int_sigaction;
sigemptyset(&int_sigaction.sa_mask);
int_sigaction.sa_flags = argc>1 ? SA_RESTART : 0;
int_sigaction.sa_handler = sig_handler;
sigaction(SIGINT,&int_sigaction,0);
printf("sa_flags=%s\n",int_sigaction.sa_flags==SA_RESTART ? "restart" : "0");
int ret = read(0,&argc,sizeof(argc));
if(ret < 0)
printf("func=read,errno=%d,error=%s\n",errno,strerror(errno));
return 0;
}
$ ./Test --restart # 此时会设置 SA_RESTART
sa_flags=restart
^C捕捉到信号,信号=SIGINT # 在执行信号处理函数之后自动重启被中断的系统调用 read(),
^C捕捉到信号,信号=SIGINT
^C捕捉到信号,信号=SIGINT
^C捕捉到信号,信号=SIGINT
^\退出 (核心已转储)
$ ./Test
sa_flags=0
^C捕捉到信号,信号=SIGINT # 在执行信号处理函数之后,read() 出错返回.如下:
func=read,errno=4,error=Interrupted system call
-
SA_NODEFER,执行信号处理程序之前,内核不自动阻塞该信号(否则,内核会将该信号添加到进程的信号屏蔽集中)
int main(int argc,char *argv[]){
struct sigaction int_sigaction;
sigemptyset(&int_sigaction.sa_mask);
int_sigaction.sa_flags = argc>1 ? SA_NODEFER : 0;
int_sigaction.sa_handler = sig_handler;
sigaction(SIGINT,&int_sigaction,0);
print_cur_sigprocmask("在调用 pause() 之前");
pause();
print_cur_sigprocmask("在调用 pause() 之后");
return 0;
}
$ ./Test
在调用 pause() 之前,屏蔽信号集: 空
^C在信号处理函数中,屏蔽信号集: SIGINT # 可以看出,在执行信号处理函数之前,内核自动阻塞了 SIGINT 信号
捕捉到信号,信号=SIGINT
在调用 pause() 之后,屏蔽信号集: 空
$ ./Test --nodefer
在调用 pause() 之前,屏蔽信号集: 空
^C在信号处理函数中,屏蔽信号集: 空 # 可以看出,在设置了 SA_NODEFER 之后,在执行信号处理函数之前,内核不会阻塞 SIGINT 信号
捕捉到信号,信号=SIGINT
在调用 pause() 之后,屏蔽信号集: 空
-
SA_RESETHAND,在执行信号处理函数,将信号的处理方式设置为默认(SIG_DFL).
void sig_handler(int s){
print_sigaction(SIGINT,"在信号处理函数中");
return;
}
int main(int argc,char *argv[]){
struct sigaction int_sigaction;
sigemptyset(&int_sigaction.sa_mask);
int_sigaction.sa_flags = argc>1 ? SA_RESETHAND : 0;
int_sigaction.sa_handler = sig_handler;
sigaction(SIGINT,&int_sigaction,0);
print_sigaction(SIGINT,"在调用 pause() 之前");
pause();
print_sigaction(SIGINT,"在调用 pause() 之后");
return 0;
}
$ ./Test
在调用 pause() 之前;信号=SIGINT;处理方式=捕捉;函数=0x400efd
^C在信号处理函数中;信号=SIGINT;处理方式=捕捉;函数=0x400efd
在调用 pause() 之后;信号=SIGINT;处理方式=捕捉;函数=0x400efd
$ ./Test --reset-handler
在调用 pause() 之前;信号=SIGINT;处理方式=捕捉;函数=0x400efd
^C在信号处理函数中;信号=SIGINT;处理方式=默认 # 此时,在调用信号处理函数之前,已经将对信号 SIGINT 的处理方式,设置为默认.
在调用 pause() 之后;信号=SIGINT;处理方式=默认
-
SA_SIGINFO,参见 sa_handler,sa_sigaction.
SA_NOCLDWAIT,此时不会产生僵尸进程,大概是这样的,当子进程终止时,内核会根据子进程的终止状态生成 siginfo_t 对象,然后调用 SIGCHLD 的信号处理函数,然后内核回收子进程,不会产生僵尸进程.
SA_NOCLDSTOP,此时丢弃子进程因为暂停,或者重新唤醒而发出 SIGCHLD 信号,即此时不会调用信号处理函数.
SA_NOCLDWAIT,SA_NOCLDSTOP 仅当为 SIGCHLD 设置信号处理方式时,才有效.
/* 解析命令行参数,获取 sigaction 中 sa_flags 的值 */
int get_sa_flags(int argc,char *argv[]){
if(argc <= 1)
return 0;
if(strcmp(argv[1],"--no-stop") == 0)
return SA_NOCLDSTOP;
if(strcmp(argv[1],"--no-wait") == 0)
return SA_NOCLDWAIT;
return SA_NOCLDSTOP|SA_NOCLDWAIT;
}
/* 这个例子演示了 SA_NOCLDWAIT,SA_NOCLDSTOP 标志 */
int main(int argc,char *argv[]){
if(safe_fork() == 0){
printf("child:pid=%d\n",getpid());
for(int i=0;i>=0;++i) ;/* 大约会花费 3.5 s */
for(int i=0;i>=0;++i) ;
exit(33);
}else{
struct sigaction chld_sigaction;
chld_sigaction.sa_flags = SA_SIGINFO|get_sa_flags(argc,argv);
sigemptyset(&chld_sigaction.sa_mask);
chld_sigaction.sa_sigaction = chld_sighandler;
sigaction(SIGCHLD,&chld_sigaction,0);
puts("parent:ready");
for(;;)
pause();
}
return 0;
}
$ ./Test #首先即不设置 NO_CLDWAIT,也不设置 NO_CLDSTOP
parent:ready
child:pid=3640
捕捉到了 SIGCHLD 信号;child_pid=3640;status=暂停;信号=SIGSTOP #在另外一个 shell 窗口运行 kill -SIGSTOP 3640,可以看到此时父进程收到了子进程因为暂停而发送的 SIGCHLD 信号.
捕捉到了 SIGCHLD 信号;child_pid=3640;status=重新唤醒;信号=SIGCONT #在另外一个 shell 窗口运行 kill -SIGCONT 3640,可以看到此时父进程收到了子进程因为暂停而发送的 SIGCHLD 信号.
捕捉到了 SIGCHLD 信号;child_pid=3640;status=正常退出;exit_code=33
# 此后在另外一个窗口运行 ps -e | grep Test,可以看到
# 3639 pts/3 00:00:00 Test
# 3640 pts/3 00:00:07 Test <defunct> 子进程变为了僵尸进程.
^C #终止父进程
$ ./Test --no-wait # 此时只设置 SA_NOCLDWAIT
parent:ready
child:pid=3648
捕捉到了 SIGCHLD 信号;child_pid=3648;status=暂停;信号=SIGSTOP # 同上,父进程会收到子进程因为暂停,或者暂停后唤醒而发出的 SIGCHLD 信号.
捕捉到了 SIGCHLD 信号;child_pid=3648;status=重新唤醒;信号=SIGCONT
捕捉到了 SIGCHLD 信号;child_pid=3648;status=正常退出;exit_code=33
# 此后在另外一个窗口运行 ps -e | grep Test,可以看到
# 3647 pts/3 00:00:00 Test 此时只有父进程,子进程没有变为僵尸进程,
^C # 终止父进程
$ ./Test --no-stop
parent:ready
child:pid=3656
# 此时父进程无法收到子进程因为暂停,或者重新唤醒发出的 SIGCHLD 信号.
捕捉到了 SIGCHLD 信号;child_pid=3656;status=正常退出;exit_code=33
^C
进程地址空间
.rodata,.data,.bss 区别:
.rodata,.data,存放的是有初始值的变量,所以需要将变量的初始值存放在可执行文件中,在调用 exec() 时,会从可执行文件中将初始值加载到进程的地址空间中.
.bss 存放的是初始值为0的变量,所以不需要将其初始值保存在可执行文件中,
int a = 33;/* 此时需要在可执行文件中占用 4 个字节来保存变量 a 的初始值 33. */
环境变量
环境字符串,结构为 Name=Value;
/** 遍历 environ 指向的环境表,查找名为 name 的环境变量,然后获取该环境变量的值.
若不存在名为 name 的环境变量,则返回 0,否则返回一个指针,指向着环境字符串的 value 部分. */
char *getenv(const char *name);
/** 修改,或者新增一个环境变量,string 的格式要求为:Name=Value,若名为 Name 的环境变量已经存在,则删除之.
该函数仅是将 string 放入环境表中,并不会为环境字符串分配空间. */
int putenv(char *string);
/** 修改,或者新增环境变量,此时会为环境字符串分配空间,然后将 name=value 复制到新分配的空间中,然后将空间的首地址存放到环境表中.
@param overwrite 指示着当名为 name 的环境变量已经存在时,是否修改该环境变量. */
int setenv(const char *name, const char *value, int overwrite);
/** 删除名为 name 的环境变量.
此时就是遍历 environ 指向的环境表,确定 name 所在的环境表项,然后从环境表中益处该项. */
int unsetenv(const char *name);
fork()对进程对象的更改
pid,ppid;即进程 ID,父进程 ID.
子进程的进程对象中,tms_utime,tms_stime,tms_cutime,tms_cstime 这几个字段被清为0.
子进程的进程对象中,剩余的闹钟时间被清为0.
子进程的进程对象中,未处理的信号集被清空.
父进程拥有的文件锁并不会被继承.
exec*()对进程对象的更改
关闭设置了 FD_CLOEXEC 标志的文件描述符.
可能会更改进程对象的有效ID,保存的设置ID.如下:
if(可执行文件.SUID==1)
进程.有效用户 ID = 可执行文件.拥有者ID.
进程.保存的设置用户ID = 进程.有效用户ID
if(可执行文件.SGID == 1)
进程.有效组 ID = 可执行文件.拥有组 ID.
进程.保存的设置组ID = 进程.有效组ID.
信号的信号处理方式,即若信号的信号处理方式为捕捉,则更改为默认.如下:
/* ./test 程序 */
int main(int argc,char *argv[]){
print_sigaction(SIGCHLD);
}
/* ./Test */
int main(int argc,char *argv[]){
struct sigaction chld_sigaction;
chld_sigaction.sa_flags = SA_SIGINFO;
sigemptyset(&chld_sigaction.sa_mask);
chld_sigaction.sa_sigaction = chld_sighandler;
sigaction(SIGCHLD,&chld_sigaction,0);
print_sigaction(SIGCHLD);
execl("./test","./test",(char*)0);
return 0;
}
$ ./Test
信号=SIGCHLD;处理方式=捕捉(SA_SIGINFO);函数=0x40115d # 在执行 execl() 之前,
信号=SIGCHLD;处理方式=默认 # execl() 将对 SIGCHLD 信号的处理方式更改为默认.