Linux下的进程(二):进程控制 - HeavyYuan/A-CD-Record-Management-System GitHub Wiki

启动进程

有三种方式来启动进程,分别是system()exec()系列函数、fork()

system效率不高,不常用

forkexec常常配合使用

system()

#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的效率不高,不是进程启动的理想手段。

exec系列函数

#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函数不会返回。

fork函数

经典代码片段

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);
}

进程终止

  1. 退出函数
#include <stdlib.h>
void exit(int status);   //退出前执行一些清理处理,然后返回内核
void _Exit(int status);  //立即进入内核

#include <unistd.h>
void _exit(int status);  //立即进入内核
  1. atexit函数
#include <stdlib.h>
int atexit(void (*func)(void));

func为注册的函数,在程序终止前执行,先注册后执行,重复注册重复执行。

  1. 退出码

如果没有调用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之前需要改回普通权限。

⚠️ **GitHub.com Fallback** ⚠️