进程控制(2)
6.执行新程序
在用fork或vfork创建子进程以后,子进程通常会调用exec函数来执行另一个程序。系统用exec用于执行一个可执行程序用于执行一个可执行程序以代替当前进程的执行映像。
先了解下什么是环境变量
argc、argv之前了解过,argc是命令行参数的个数,argv是字符串数组,其中保存着命令行参数
envp是环境变量的值,我们可以通过一个例子了解下。
//env.c
#include<stdio.h>
#include<memory.h>
extern char **environ;//系统预定的全局变量显示各个环境变量的值
int main(int argc, char *argv[], char ** envp)
{
int i;
printf("Argument:\n");
for(i=0; i<argc; ++i)
{
printf("argv[%d] is %s\n", i, argv[i]);
}
printf("Environment1:\n");
for(i=0; environ[i]!=NULL; ++i)
printf("%s\n", environ[i]);
printf("################俺是分割线##################\n");
printf("\n\n\nEnvironment2:\n");
for(i=0; envp[i]!=NULL; ++i)
printf("%s\n", envp[i]);
return 0;
}
一张图放不下,分成了两张图
是不是在环境变量里发现一些眼熟的东西?😏
ps:Linux终端使用env可查看环境变量的值(和上边一样)
ps:Linux引入环境变量的概念,包括用户的主目录,终端类型、当前目录,他们定义了用户的工作环境,所以就叫环境变量。
ps:exec调用并未生成新的进程,一个进程一旦调用exec函数,其本身就已经“死亡”了,系统把代码段替换成新的程序的代码,废弃原有数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一保留的就是进程ID,也就是说,对系统而言,还是同一个进程,只不过执行的程序是另一个了。
exec函数族
#include<unsitd.h>
int execve(const char *path, char *const argv[], char *const envp[]);
//在该系统调用中,参数path是将要执行的程序的路径名,参数argv、envp与main函数内的argv,envp对应
int execv(const char* path, char *const envp[]);
//通过路径名方式调用可执行文件作为新的n进程映像。它的argv参数用来提供给main函数的argv参数,argv参数是一个以NULL结尾的字符串数组(最后一个元素必须是空指针)
int execle(const char* path, const char *arg, ...);
//该函数与execl函数用法类似。只是要显式指定环境变量。环境变量位于命令行参数最后一个参数的后面,也就是位于空指针之后。
int execl(const char* path, const char *arg, ...);
//该函数与execv函数类似,只是在传递参数argv的时候,每个命令行参数都声明为一个单独的参数,(参数中使用"..."参数个数代表不确定),但要注意的是这些参数要以一个空指针结束。
int exevp(const char* file, char *const argv[]);
//该函数和execv函数用法类似。不同的是参数filename。该参数如果包含"/",就相当于路径名;如果不包含"/",函数就到PATH环境变量定义的目录中找寻可执行文件。
int execlp(const char* file, const char *arg, ...);
//该函数类似execl函数,他们的区别和execvp与execv区别一样。
ps:exce函数族的6个函数只有execve是系统调用。其他5个都是库函数,他们实现时都调用了execve
正常情况下,这写函数无返回值,由于进程的执行映像已经被替换,无接收返回值的地方。假设有一个错误事件,将会返回-1,一般是由于文件名或参数引起的错误。
示例程序
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(int argc, char *argv[], char **environ)
{
pid_t pid;
int stat_val;
printf("Exec Example!\n");
pid = fork();
switch(pid)
{
case -1:
perror("Process Creation Error!\n");
exit(1);
case 0:
printf("Child process is running\n");
printf("My pid is %d, parent pid is %d\n",getpid(), getppid());
printf("uid = %d, gid = %d\n",getuid(), getgid());
execve("main", argv, environ);//这里mian是学数据结构链栈生成的可执行文件
printf("process never come here\n");
exit(0);
default:
printf("Parent process is running\n");
break;
}
wait(&stat_val);
exit(0);
}
执行结果
使用exec执行新程序一后进程除了保存原来进程ID,父进程ID,实际用户ID和实际组ID之外,进程还保持了许多原有特征,比如
·当前工作目录
·根目录
·创建文件时使用的屏蔽字
·进程信号屏蔽字
·未决警告
·与进程相关的使用处理器的时间
·控制终端
·文件锁
7.等待进程结束
子进程如果比父进程先退出且父进程未使用wait或waitpid函数,子进程就会成为僵进程。wait和waitpid函数可以有效防止子进程成为僵进程。
声明如下
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
wait函数使父进程暂停执行,直到他的一个子进程结束为止。该函数的返回值是子进程的PID。参数statloc所指向的变量存放子进程的退出码,就是子进程mian函数退出时的返回值(例如exit(-1),return 0),如果statloc不是一个 空指针,状态信息将被写入它之的变量。
waitpid也用来等待子进程的结束,但是它用于等待某个特定进程结束。参数pid指明要等待子进程i的PID,pid值的意义见下表。参数statloc意义同wait。options参数允许用户改变waitpid的行为,若将该参数赋值为WNOHANG,则使父进程不被挂起而立即返回并执行其后的代码。
waitpid(child_pid, (int*)0, WNOHANG)可以让父进程周期性的检查某个特定子进程是否已经退出。若子进程未退出,它将返回0;如果子进程已经结束,则返回child_pid。调用失败时返回-1。失败的原因可能是该子进程不存在、参数不合法等。
sys/wait.h中定义了wait和waitpid的宏
例程
#include<sys/types.h>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
int pid;
const char *msg;
int k;
int exit_code;
printf("Study how to use wait function!\n");
pid = fork();
switch(pid)
{
case -1:
printf("Process creat failed\n");
exit(-1);
case 0:
msg = "Child process is running";
k = 5;
exit_code = 37;
break;
default:
exit_code = 0;
break;
}
if(pid!=0)//父进程会在这里等待子进程结束
{
int stat_val;
int child_pid;
child_pid = wait(&stat_val);
printf("Child process has exited, pid = %d, parentpid = %d\n", child_pid, getpid());
if(WIFEXITED(stat_val))
printf("Child exited with code %d\n", WEXITSTATUS(stat_val));//WEXITSTATUS会得到子进程eixt或return时的值
else
{
printf("Child exited abnormaly\n");
}
}
else
{
while(k-->0)
{
puts(msg);
sleep(1);
}
}
exit(exit_code);
}
打开另一个终端用ps aux命令可以看到父进程的状态
S状态是什么意思?
ps -eo pid, stat命令可以查看当前进程状态。
R(runnable)运行状态,S(sleeping)可中断等待状态,D(uninterruptible sleep)不可中断等待状态,Z(zombile)僵死状态,T(teaced stopped)停止状态
状态中还有一些字符后缀,其意义分别是:<(高优先级程序),N(低优先级程序),L(内存锁页,即页不可被换出内存),s(该进程为会话首进程),l(多线程进程),+(进程位于前台进程组)。例如,Sal说明该进程处于可中断等待状态,且该进程为会话首进程,而且是一个多线程进程。
ps:wait等待第一个终止的子进程,而waitpid则可以指定等待特定的子进程。waitpid提供了wait的非阻塞版本。
二、
1.获得进程ID
系统调用getpid()用来获取当前进程ID.
声明如下
#include<sys/types.h>
#include<unistd.h>
pit_t getpid(void);//获取当前进程ID号
getppid()可以获取父进程ID。
2.setuid和setgid
setuid设置实际用户ID和有效用户ID。setgid用来设置实际组ID和有效组ID。
声明如下
#include<sys/types.h>
#include<unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid)
首先搞清楚四个概念:
·实际用户ID(uid):标识运行该进程的用户。
·有效用户ID(euid):标识以什么用户身份来运行进程。例如,某个普通用户A,运行了一个进程,而这个进程是以root身份来运行的,这个程序运行时就具有root权限。此时实际用户ID是A用户的ID,而有效用户ID是root用户ID。
·实际组ID(gid):它是实际用户所属的组的组ID。
·有效组(egid):有效组ID是有效用户所属的组的组ID。
设置用户ID的setuid函数遵守以下规则
·如果进程有root权限,则函数将实际用户ID、有效用户ID设置为参数uid。
·若进程不具有root权限,但uid等于实际用户ID,则setuid只将有效用户ID设置为uid,不改变实际用户ID。
·若以上两个条件都不能满足;则函数调用失败,返回-1,并设置errno为EPERM。
从上述规则可以看出来,仅超级用户进程才能更改实际用户ID。所以一个非特权用户进程是不能通过setuid或setuid得到特权用户权限的。但是su命令却可以让一个普通用户变为一个特权用户。这是因为su是一个“set_uid”程序。执行一个设置了set_uid位的程序时,内核进程的有效用户ID设置为文件属性的ID。而内核检查一个进程是否具有访问某个文件的权限时,是使用进程的有效用户ID来进行检查的。su程序的文件属主是root,普通用户运行su命令时,su程序的权限是root权限。
例程
//studyuid.c
#include<stdio.h> #include<stdlib.h> #include<sys/types.h> #include<unistd.h> #include<fcntl.h> #include<errno.h> #include<string.h> extern int errno; int main() { int fd; printf("uid study:\n"); printf("Process's uid = %d, euid = %d\n", getuid(), geteuid()); if((fd = open("test.c", O_RDWR)) == -1) { printf("Open failure, errno is %d :%s \n", errno, strerror(errno)); exit(1); } else { printf("Open successfully!\n"); } close(fd); exit(0); }
首先在root用户下新建test.c文件,然后你可以用ll命令查看test.c、studyuid文件的权限
然后运行studyuid.c生成的可执行文件
ps:root用户的uid和euid均为0
接着切换普通用户来运行发现权限不足
这时候,我们再切回root用户,输入一行命令
chmod 4775 studyuid
这时候你再ll查看studyuid的权限,发现变为
x(可执行)权限变成了s(SUID,Set UID)权限,而且文件本身也变成了红色
SUID,Set UID权限:可执行的文件搭配这个权限,便能得到特权,任意存取该文件的所有者能使用的全部系统资源。请注意具备SUID权限的文件,黑客经常利用这种权限,以SUID配上root帐号拥有者,无声无息地在系统中开扇后门,供日后进出使用。
这时候我们再切回普通用户再执行studyuid文件
发现可以成功运行。
问题的关键就是刚刚那个chmod命令,root用户为studyuid文件增加了s权限,则其他用户在执行studyuid文件时也会具有root用户的相应权限,这也就是euid为何变成0的原因。
suid意味着如果某个用户对属于自己的可执行文件设置了这种权限,那么其他用户在执行这个文件时也会具有其属主的相应权限。
guid则表示执行相应可执行文件的用户将具有该文件所属用户组中用户的权限。
如何设置suid和guid:
设置suid就是把0变为4,设置guid就把0变为2,如果都设置那就是6了
内核对进程存取文件的许可权的检查,是通过考察进程的有效用户ID来实现的。(红色即表示该文件有效用户ID为root)
setuid函数用法:开始时某个程序要root权限完成一些工作,可是后续工作不需要root权限。可以将该可执行程序文件设置root权限,当在不需要root权限时,调用setuid(getuid())恢复进程的实际用户ID和有效用户ID为执行该程序的普通用户的ID。这个在一些提供网络服务的进程非常重要。
3.改变进程的优先级
在Linux下,通过系统调用nice可以改变进程的优先级。nice系统调用用来改变进程的优先级。
声明如下
#include<unsitd.h>
int nice(int increment)
首先认识两个函数
#include<sys/resource.h>
int getpriority(int which, int who);
该函数返回一组进程的优先级,参数which和who组合确定返回哪一组进程的优先级。which的可能取值以及who的意义如下
·PROI_PROCESS:一个特定的进程,此时who的取值为进程ID。
·PRIO_PGRP:一个进程组的所有进程,此时who的取值为进程ID。
·PRIO_USER:一个用户拥有的所有进程,此时参数who取值为实际用户ID。
getpriority函数如果调用成功返回指定进程的优先级,如果出错将返回-1,并设置errno的值。errnos取值如下
·ESRCH:which和who的组合与现存的所有进程均不匹配。
·EINVAL:which 是个无效的值
ps:当指定的一组进程的优先级不同时,getpriority将返回其中优先级最低的一个。此外,当getpriority返回-1时,可能发生错误,也有可能是返回的是指定进程的优先级。区分它们唯一的方法是在调用getpriority前将errno清零。若返回值为-1且errnow不为0,说明程序错误。
#include<sys/resource.h>
int setpriority(int which, int who, int prio);
该函数用来指定进程的优先级。进程指定的方法与getpriority函数相同。如果调用成功,函数返回指定进程的优先级,出错则返回-1。并设置相应的errno。除了产生与getpriority相同的两个错误外,还有可能产生以下错误。
·EPERM:要设置优先级的进程与当前进程不属于同一个用户,并且当前进程没有CAP_SYS_NICE特许。
·EACCES:该调用可能降低进程的优先级并且没有CAP_SYS_NICEd特许。
通过以上两个函数就可以改变进程优先级,nice系统调用是他们的一种组合形式,nice系统调用等价于
int nice(int increment)
{
int oldpro = getpriority(PRIO_PROCESS, getpid());
return setpriority(PRIO_PROCESS, getpid(), oldpid(), oldpro+increment);
}
例程
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/resource.h>
#include<sys/wait.h>
int main()
{
int pid;
int stat_val = 0;
int oldpri, newpri;
printf("nice study\n");
pid = fork();
switch(pid)
{
case 0:
printf("Child is running, CurPid is %d, ParentPid is %d\n", pid, getppid());
oldpri = getpriority(PRIO_PROCESS, 0);
printf("Old priority = %d\n", oldpri);
newpri = nice(3);//为子进程增加优先级
printf("New priority = %d\n", newpri);
exit(0);
case -1:
perror("Processs creation failed\n");
break;
default:
printf("Parent is running ,ChildPid is %d, ParentPid is %d\n", pid, getpid());
break;
}
wait(&stat_val);
exit(0);
}
成功运行时的Ubuntu版本为18.04.2
左侧版本运行时总是返回-1,右侧运行成功
失败和成功的结果如下
失败
成功