Linux下的进程(二):进程控制 - HeavyYuan/A-CD-Record-Management-System GitHub Wiki
有三种方式来启动进程,分别是system()
、 exec()
系列函数、fork()
system
效率不高,不常用
fork
和exec
常常配合使用
#include <stdlib.h>
#include <stdio.h>
int main()
{
printf("Running ps with system\n");
system("ps -ef");
printf("Done.\n");
exit(0);
}
system
的执行方式是阻塞式的,其需要等待所启动的程序运行结束后才返回,才能执行system
之后的代码(printf)。
system
必须用一个shell来启动需要的程序,所以在启动需要的程序前需要先启动一个shell
因此,system
的效率不高,不是进程启动的理想手段。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
printf("Running ps with system\n");
execlp("ps","ps","-ef",0);
printf("Done.\n");
exit(0);
}
exec函数会替换当前进程映像,当前程序代码的exec函数之后的代码都不在执行。
即不会输出Done
除非发生错误,exec函数不会返回。
经典代码片段
pid new_pid;
new_pid = fork();
switch(new_pid){
case -1: break; /*Error*/
case 0 : break; /*子进程上下文*/
default: break; /*父进程上下文*/
}
执行fork(),则复制出了一个子进程(可以ps看到),父子进程的代码完全一样。
父子进程在执行switch
代码时,根据fork()
的返回值来区分父子进程上下文。
在子进程中,我们可以执行exec函数,替换子进程的映像,进化成有其他功能的进程。
#include <sys/types.h>
#include <sys/wait.h>
pid_twait(int *stat_loc); /*等待子进程结束*/
pid_t waitpid(pid_t pid, int *stat_loc, int options); /*等待进程pid结束*/
进程已经退出,但是其资源还没有释放。
常见场景是,子进程 比 父进程 先执行完,但是子进程的进程信息还可能被父进程用到,因此子进程的进程信息没有释放,导致子进程成为为僵尸进程(defunct/zombie)
当父进程运行完后,资源完全释放,僵尸进程就消失了。
以下程序会产生僵尸子进程
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid;
char *message;
int n;
printf("fork program starting\n");
pid = fork();
switch(pid)
{
case -1:
perror("fork failed");
exit(1);
case 0:
message = "This is the child";
n = 3;
break;
default:
message = "This is the parent";
n = 10;
break;
}
for(; n > 0; n--) {
puts(message);
sleep(1);
}
exit(0);
}
- 退出函数
#include <stdlib.h>
void exit(int status); //退出前执行一些清理处理,然后返回内核
void _Exit(int status); //立即进入内核
#include <unistd.h>
void _exit(int status); //立即进入内核
-
atexit
函数
#include <stdlib.h>
int atexit(void (*func)(void));
func为注册的函数,在程序终止前执行,先注册后执行,重复注册重复执行。
- 退出码
如果没有调用exit和return
程序退出码是main函数中最后一个函数的返回码
#include <stdio.h>
int main()
{
printf("hello, world\n");
}
以上程序的返回值是printf的返回值13(输出字符的个数)
进程权限控制基于用户ID和组ID
以下以用户ID的细节,组ID同样适用
有三种用户ID: real user ID(ruid)
、effective user uid(euid)
和 saved set-user-ID(suid)
ruid : 实际用户ID,表示我们实际上是谁,登陆系统时用的用户名对应的id,在系统内通过id命令可以查看到的uid
euid : 有效用户ID,用于文件访问权限检查
suid : 保存的设置用户ID,程序执行时,由exec函数保存的euid的副本
进程可以通过getxxuid/setxxuid一系列函数来获取/更改这些ID。
密码修改调用的是/usr/bin/passwd
程序,其需要读写/etc/shadow
文件,而该文件只能被具有超级用户特权的进程读写。
普通用户可以修改密码的必要条件:
1./usr/bin/passwd
属于root用户
2.passwd
程序文件增加了s权限,即设置了set-user-ID位(在root下执行:chmod u+s /usr/bin/passwd
)
在以上两个条件下,passwd
可以具有读写/etc/shadow
的权限。
当文件设置了s权限,表明在执行该文件时,进程的euid会被设置成该文件所有者的用户ID(本案例时root的用户ID=0)
at
程序被用来设定计划任务:在未来某个时刻运行某个程序,atd
在设定的时间运行特定的程序。
at和atd文件都属于root用户, at在设定任务时读写的都是root的用户的配置文件,因此at具有s权限,便于普通用户来设定他们各自的任务。
在at程序的执行过程中,其首先会消减特权,只有当要访问配置文件是,at才会再次升级特权。
对于atd也一样,其本身是root权限(cat /proc/pid/status/可以看到r/e/suid),其最终会执行普通用户的程序,根据最小特权模型,
atd根据配置文件,将fork出进程A来执行普通用户的程序,并将A进程的r/e/suid都设置成普通用户的uid,防止对特权的误用
fork出的进程是继承了父进程的各个用户ID,如果在“提升权限”场景下,exec后的子进程euid也是等于0
所以在fork之后,exec之前需要改回普通权限。