《Unix环境高级编程》之 进程控制

  1. 进程标识符
      每个进程都有非负整型表示的唯一进程ID,虽然进程ID唯一,但是ID可以重用,大多数Unix系统采用延迟重用技术,即保证赋予给新建进程的ID不同于最近终止进程所使用的ID。
       ID为0的进程通常是调度进程,也被称为交换进程,是内核的一部分,也被称为系统进程。ID为1的进程通常是init进程,在自举过程(加载内核)结束后由内核调用,该进程的程序文件一般为/sbin/init,init通常读与系统有关的初始化文件(/etc文件夹下),并将系统引导至一个状态,init进程不会终止,它是一个普通的用户进程,但是以超级用户权限运行。
    下列函数返回与进程相关的标识符:

    
    #include <unistd.h>
    
    pid_t getpid(void); 返回值:调用者的进程id;
    
    pid_t getppid(void); 返回值:调用者的父进程的id;
    
    uid_t getuid(void); 返回值:调用进程的实际用户id;
    
    uid_t geteuid(void); 返回值:调用进程的有效用户id;
    
    uid_t getgid(void); 返回值:调用进程的实际组id;
    
    uid_t getugid(void); 返回值:调用进程的有效组id;
    
  2. fork函数
      使用fork函数创建新进程:

        #include <unistd.h>
    
        pid_t fork(void);
        返回值:子进程中返回0, 父进程中返回子进程ID,出错返回-1

      子进程获得父进程的数据空间、堆和栈的副本,但是父子进程共享代码正文段。由于fork之后经常跟随exec,所以现在很多系统实现中并不执行一个父进程数据的、栈和堆全部复制,而是使用了写时复制,这些区域由父子进程共享,只不过将访问权限改为只读的,当父子中任意一个进程试图修改这些区域,则内核为那一块区域制作一个副本。Linux系统提供了clone系统调用,它是fork的一种泛型,使调用者控制哪部分由父子进程共享,Linux系统中的线程实际上也是由clone实现的。
      fork的另一个特性是父进程的所有打开文件描述符都被复制到子进程中,父子进程每个相同的文件描述符共享同一个文件表项,如下图所示:
      
      还在路上,稍等...

    除了打开文件外,父进程的很多其它属性也由子进程继承,包括:各种用户ID和组ID、会话ID、控制终端、设置用户ID标志和设置组ID标志、当前工作目录、根目录、文件模式创建屏蔽字、信号屏蔽和安排、环境等。

  3. vfork函数
    vfork的调用序列和返回值和fork相同,但两者的意义不同。vfork也创建一个新进程,但该新进程的目的是为了exec一个新程序,vfork并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec或exit,vfork保证子进程先运行,在它调用exec后父进程才可能被调用运行。

  4. exit函数
    进程有下列五种正常终止方式:

    • 在main函数内执行return语句,等效于调用exit;
    • 调用exit函数,此函数由ISO定义,其操作包括调用各终止处理程序,然后关闭所有标准I/O流;
    • 调用_exit或_Exit函数。_exit提供一种无需运行终止处理程序或信号处理程序而终止的方法,_exit由exit函数调用,在大多数实现中exit是标准C库的函数,而_exit则是一种系统调用;
    • 进程的最后一个线程在其启动例程中执行返回语句;
    • 进程的最后一个线程调用pthread_exit函数。

    三种异常终止方式如下:

    • 调用abort,产生SIGABRT信号;
    • 当进程接受到某些信号时;
    • 最后一个线程对“取消”请求做出响应时。

    不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开文件描述符,释放它使用的存储器等。终止进程的父进程可以通过wait或waitpid函数取得其终止状态。
      对于父进程已终止的所有进程,它们的父进程都改变为init进程,操作过程大致如下:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进程的子进程,如果是,则将其父进程ID更改为1。
      子进程终止时,内核为每个子进程保存了一定量的信息,包括进程ID、进程的终止状态、以及该进程使用的CPU时间总量,该子进程的父进程可以通过wait等取得其终止状态,父进程获取子进程的终止状态后,内核才会释放子进程使用的存储资源。一个已终止,但其父进程未获取其终止状态的进程,被称为僵死进程。由init进程领养的子进程不会变成僵死进程,因为init进程被设计为无论何时只要有一个子进程终止,就会调用wait获取其终止状态。

  5. wait和waitpid函数
      当进程正常或异常终止时,内核就向其父进程发送SIGCHILD信号。调用wait和waipid时可能会发生以下几种情况:① 如果所有子进程都还在运行,则发生阻塞;② 如果一个子进程已终止,正在等待获取其终止状态,则获取其终止状态;③ 如果它没有任何子进程,则立即出错返回。

    
    #include <sys/wait.h>
    
    pid_t wait(int * staloc);
    pid_t waitpid(pid_t pid, int * staloc, int options);
    返回值: 若成功则返回进程ID,出错返回-1

      这两个函数的区别是:wait使调用者阻塞,而waitpid则有一个选项可以控制是否阻塞;waitpid不等待其调用之后的第一个终止进程,它可以控制它想等待的进程。
      参数staloc保存终止进程的终止状态,若不关心终止状态,则定义为空。
      waitpid中参数pid的解释如下:若pid==-1,则等待任一子进程,与wait功能相同;若pid<-1,则等待组ID等于pid绝对值的任一子进程;若pid>0,等待进程ID和pid相等的子进程;若pid==0,则等待组ID等于调用进程的组ID的任一子进程。若pid指定的进程不是调用进程的子进程,则waitpid会出错。

  6. exec函数
    当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,并从新程序的main函数处开始执行。exec只是用一个新程序替换了当前进程的正文段、数据段,堆和栈。
    fork、exec、exit和wait是基本的进程控制原语。有六种exec函数可供调用:

    
    #include <unistd.h>
    
    int execl(const char* pathname, const char* arg0, .../*(char *)0 */);
    int execv(const char* pathname, char *const argv[]);
    int execle(const char* pathname, consts char* arg0, .../*(char *)0, char* const envp[] */);
    int execve(const char* pathname, char *const argv[], char* const envp[]);
    int execlp(const char* filename, consts char* arg0, .../*(char *)0*/);
    int execvp(const char* filename, char *const argv[]);
    
    6个函数返回值:若出错返回-1,若成功则不返回值。

      前四个函数取新程序的路径名为参数,后两个则取文件名为参数,如果pathname中包含了/,则将其视为路径名,否则按PATH环境变量,在指定的目录中搜寻可执行文件。PATH环境变量包含了一张目录表(称为路径前缀),目录之间用冒号分割,其中零长前缀表示当前目录,如PATH = /bin::/user/bin:/user/local/bin,其中:: 表示当前目录。
      如果找到了可执行文件,但该文件不是连接器生成的机器可执行文件,则认为该文件是一个shell脚本,于是试着调用/bin/sh来执行。
      另一个区别与参数表的传递有关(函数名中的l表示list,v表示矢量数组vector),函数execl,execlp,execle都要求将新程序的每个命令行参数说明为一个单独的参数,这种参数表以空指针结束。
      函数名中带e的表示需要向新程序传递环境表,execle和execve可以向新程序传递一个指向环境字符串的指针数组,其它四个函数则使用调用进程中的environ变量为新程序复制现有的环境。
      对于原进程中打开的文件描述符,与每个描述符的执行时关闭(FD_CLOEXEC)标志值有关,若此值设置,则在执行exec函数时,关闭该描述符,否则该描述符仍打开,系统默认是关闭该标志。
      执行exec函数后,进程的实际用户ID和组ID保持不变,但有效ID是否变化取决于所执行程序文件的设置用户ID位是否设置
      在很多UNIX系统中,这6个函数只有execve是系统调用,其它函数都要调用该系统调用。

  7. 更改用户ID和组ID
    可以用setuid函数设置实际用户ID和有效用户ID,类似的可以用setgid设置实际 组ID和有效组ID。

    
    #include <unistd.h>
    
    int setuid(uid_t uid);
    int setgid(gid_t gid);
    返回值:若成功返回0,出错返回-1

    更改用户ID的规则如下:

    • 若进程具有超级用户特权,则setuid将实际用户ID、有效用户ID和保存的设置用户ID设置为uid;
    • 若进程没有超级用户特权,但是uid等于实际用户ID或者保存的设置用户ID,则setuid只将有效用户ID设置为uid,而不更改实际用户ID和保存的设置用户ID;
    • 若两个条件都不满足,则将errno设置为EPERM,并返回-1。

    保存的设置用户ID位是由exec复制有效用户ID得来的,如果设置了程序文件的设置用户ID位,则exec根据文件的用户ID设置了有效用户ID后,就将这个副本保存起来。
    更改进程三个用户ID的方法:

    这里写图片描述

  8. system函数
      system函数执行一个命令字符串:

    
    #include <stdlib.h>
    
    int system(const char* cmdstring);

    如果参数cmdstring为空,则仅当system函数在当前系统上是可用时,才返回非0值。
    因为system在其中调用了fork、exec和waitpid,因此可能有三种返回值:

    • 如果fork失败或者waitpid返回除SIGINTR之外的出错时,返回-1,并在errno中设置错误返回类型;
    • 如果exec失败,则表示不能执行shell,返回值如同在shell执行了exit(127)一样。
    • 否则所有三个函数都执行成功,返回值是shell的终止状态。

    在设置用户ID位的程序中不能执行system函数,这是一个安全漏洞,因为在system内部执行exec时会将原进程的有效用户ID保持下来。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值