进程控制
进程标识
每一个进程都有一个非负整形表示的唯一进程ID。因为进程ID标识符总是唯一的,常将其用来作其他标识符的一部分以保证其唯一性。例如,应用程序有时就把进程ID作为名字的一部分来创建一个唯一的文件名。
虽然是唯一的,但是进程ID是可以复用的。当一个进程终止后,其进程ID就成为复用的候选者。大多数UNIX系统实现延迟复用算法,使得赋予新建进程的ID不同于最近终止进程所使用的的ID。
系统中有一些专用进程。ID为0的通常是调度进程,常常被称为交换进程(swapper)。该进程是内核的一部分,他并不执行任何磁盘上的程序,因此也被称为系统进程。进程ID 1通常是init进程,在自举过程结束时由内核调用。该进程的程序文件在UNIX的早期版本中是/etc/init,在较新的版本中是/sbin/init。此进程负责在在自举内核后启动一个UNIX系统。init通常读取与系统有关的初始化文件(/etc/rc*文件或者/etc/inittab文件,以及在/etc/init.d中的文件),并将系统引导到一个状态(如多用户)。init进程决不会终止。他是一个普通的用户进程,但是以超级用户特权运行。
每个UNIX系统实现都有他自己的一套提供操作系统服务的内核进程。例如,在某些UNIX的虚拟存储实现中,进程ID 2是页守护进程(page daemon),此进程负责支持虚拟存储器系统的分页操作。
#include <unistd.h> pid_t getpid(void); //返回值:调用进程的进程ID pid_t getppid(void); //返回值:调用进程的父进程ID uid_t getuid(void); //返回值:调用进程的实际用户ID uid_t geteuid(void); //返回值:调用进程的有效用户ID gid_t getgid(void); //返回值:调用进程的实际组ID gid_t getegid(void); //返回值:调用进程的有效组ID //以上函数都无出错返回
fork
#include <unistd.h> pid_t fork(void); //返回值:子进程返回0,父进程返回子进程ID,若出错,返回-1
由fork创建的新进程被称为子进程(child process)。fork函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值是新建子进程的进程ID。
子进程和父进程继续执行fork调用之后IDE指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段。 由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、站和堆的完全副本。作为替代,使用了写时复制(Copy-On_Write,COW)技术。这些区域由父进程和子进程共享,而且内核将他们的访问权限改变为只读。如果父进程和子进程中任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一“页”。
fork的开销实际就是复制了父进程的页表以及给子进程创建的唯一进程描述符,一般情况下fork后马上运行一个新的进程,这避免了拷贝大量用不到的数据。
#include <stdio.h> #include <unistd.h> int global_var = 6; char buf[] = "a write to stdout\n"; int main(){ int var; pid_t pid; var = 88; if(write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1){ printf("write error!\n"); } printf("before fork\n"); if((pid = fork()) < 0){ printf("fork error!\n"); } else if (pid == 0) { global_var++; var++; printf("ppid = %ld\n", (long)getppid()); } else { sleep(2); } printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), global_var, var); return 0; }
可以看到当我们将输出重定向到temp.out文件后多出个before fork的输出。write函数是不带缓存的。因为在fork之前调用write,所以其数据写到标准输出一次。但是标准IO是带缓存的。如果标准输出连到终端设备,则它是行缓存,否则它是全缓存。当以交互方式运行该程序时,只得到printf输出的行一次,其原因是标准输出缓存由新行符刷新。当我们将printf("before fork \n");后的换行符去掉之后即printf("before fork");来验证这一点,修改之后输出结果是:
可以看到before fork打印了两次,这说明因为我们去掉了换行符所以标准输出流的行缓存不会被flush。
但是当将标注输出重新定向到一个文件时,却得到printf输出行两次。其原因是,将标准输出重新定向到一个文件时标准输出流就不是行缓存而是全缓存了,在fork之前调用了printf一次,但当调用fork时,该行数据仍在缓存中,然后在父进程数据空间复制到子进程的过程中时,该缓存数据也被复制到了子进程中。于是那时父、子进程各自有了带该行内容的缓存。在exit之前的第二个printf将其数据添加到现存的缓冲中。当每个进程终止时,缓存中的内容将被写到相应文件中。
文件共享
对于上面的程序需要注意:在重定向父进程的标准输出时也重定向了子进程的标准输出。fork的一个特性是所有由父进程打开的文件描述符都被复制到子进程中。父、子进程每个相同的打开文件描述符共享一个文件表项。
这种共享文件的方式使父子进程对同一文件使用了一个文件位移量。对于以下情况:
一个进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父、子进程都向标准输出执行写操作。如果父进程使其标准输出重定向(很可能是由shell实现的),那么子进程写到该标准输出时,他将更新与父进程共享的该文件的位移量。在我们所考虑的例子中,当父进程等待子进程时,子进程写到标准输出;而在子进程终止后,父进程也写入到标准输出上,并且知道其输出会添加在子进程所写数据之后。如果父、子进程不共享同一文件位移量,这种形式的交互就很难实现。
如果父、子进程写到同一文件描述符文件,但又没有任何形式的同步(例如使父进程等待子进程),那么它们的输出就会相互混合(假定所用的文件描述符是在fork之前打开的)。
在fork之后处理文件描述符有两种常见的情况:
- 父进程等待子进程完成。这种情况下,父进程无需对其描述符做任何处理。
- 父、子进程各自执行不同的程序段。在这种情况下,在fork之后,父、子进程各自它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。
除了打开文件之外,很多父进程的其他性质也会由子进程继承:
- 实际用户ID、实际组ID、有效用户ID、有效组ID。
- 添加组ID。
- 进程组ID。
- 对话期ID。
- 控制终端。
- 设置-用户-ID标志和设置-组-ID标志。
- 当前工作目录。
- 根目录。
- 文件方式创建屏蔽字。(umask)
- 信号屏蔽和排列。
- 对任一打开文件描述符的在执行时关闭标志。
- 环境。
- 连接的共享存储段。
父、子进程之间的区别是:
- fork的返回值。
- 进程ID
- 不同的父进程iD。
- 子进程的tms_utime,tms_stime,tms_cutime以及tms_ustime设置为0。
- 父进程设置的锁,子进程不继承。
- 子进程的未决告警被清除。
- 子进程的未决信号集设置被清除。
使用fork失败的原因主要有两个
- 系统中已经有了太多的进程
- 该实际用户ID的进程总数超过了系统限制。
fork有以下两种用法
- 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的–父进程等待客户端服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
- 一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。 使得子进程在fork和exec之间可以更改自己的属性,如I/O重定向,用户ID、信号安排等。
fork源码
创建进程分两步:1.fork拷贝当前进程创建一个子进程,与父进程的区别仅仅是pid、ppid和某些资源和统计量(挂起的信号没必要继承);2.exec。
fork() 、vfork、_clone这些都是根据自己的需要去调clone,由clone调do_fork。do_fork调用copy_process,然后让进程开始运行
1.在copy_process内,通过dup_task_struct为进程创建一个内核栈、thread_info结构、task_struct,这些值与进程的当前值相同,此时父进程和子进程的描述符完全相同。
/** * 创建进程描述符以及子进程执行所需要的所有其他数据结构 * 它的参数与do_fork相同。外加子进程的PID。 */ static task_t *copy_process(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr, int pid) { int retval; struct task_struct *p = NULL; /** * 检查clone_flags所传标志的一致性。 */ /** * 如果CLONE_NEWNS和CLONE_FS标志都被设置,返回错误 */ if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS)) return ERR_PTR(-EINVAL); /* * Thread groups must share signals as well, and detached threads * can only be started up within the thread group. */ /** * CLONE_THREAD标志被设置,并且CLONE_SIGHAND没有设置。 * (同一线程组中的轻量级进程必须共享信号) */ if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND)) return ERR_PTR(-EINVAL); /* * Shared signal handlers imply shared VM. By way of the above, * thread groups also imply shared VM. Blocking this case allows * for various simplifications in other code. */ /** * CLONE_SIGHAND被设置,但是CLONE_VM没有设置。 * (共享信号处理程序的轻量级进程也必须共享内存描述符) */ if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM)) return ERR_PTR(-EINVAL); /** * 通过调用security_task_create以及稍后调用security_task_alloc执行所有附加的安全检查。 * LINUX2.6提供扩展安全性的钩子函数,与传统unix相比,它具有更加强壮的安全模型。 */ retval = security_task_create(clone_flags); if (retval) goto fork_out; retval = -ENOMEM; /** * 调用dup_task_struct为子进程获取进程描述符。 */ p = dup_task_struct(current); }
2.检查子进程创建后当前用户拥有的进程数目有没有超过给其分配的资源限制
3.子进程使自己与父进程区分开,进程描述符内需多值被设为0或初始值,进程描述符的成员并不是继承来的,而主要是统计信息。进程描述符中大多数数据是共享的。
4.进程状态设为TASK_UNINTERRUPTIBLE保证其不会投入运行
5.copy_process调用copy_flags更新task_struct的flags成员。表明进程是否有超级用户权限PF_SUPERFRIV标志被清0,表明进程还没有调用exec函数的PF_FORKNOEXEC标志被设置
6.调用get_pid()为新进程获取一个有效的pid
7.根据传递给clone()的标志,copy_process()拷贝或共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间。一般情况下,这些资源被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此拷贝到这里
8.子进程和父进程平分时间片
9.copy_process扫尾工作返回一个指向子进程的指针
10.再回到do_fork,如果copy_process函数返回成功,新创建的子进程唤醒并投入运行,内核有意选择让子进程先执行,因为子进程一般马上调用exec,这样可以避免写时拷贝的资源开销,如果父进程先执行,有可能会开始向地址空间写入。
vfork
vfork函数的调用序列和返回值与fork相同,除了不拷贝父进程的页表项,子进程作为父进程的一个单独线程运行在他的地址空间,父进程被阻塞,直到子进程退出或执行exec(如果在调用exec/exit之前子进程依赖于父进程的进一步动作,则会导致死锁),子进程不能向地址空间写入。
如果子进程修改数据、进行函数调用、或者没有调用exec或exit就返回都可能带来未知的结果。
- vfork的实现是通过向clone系统调用传递一个特殊的标志来进行
- 调用copy_process时,task_struct的vfork_done成员被设为NULL
- 在执行do_frok时,如果指定特殊标志,则vfork_done会指定一个特殊地址
- 子进程开始执行后,父进程不是马上恢复执行,而是一直等待,知道子进程通过vfork_done指针想他发送信号
- 调用mm_release时,该函数用于进程退出内存地址空间,并检查vfork_done是否为空,如果不为空,则会向他的父进程发送信号
- 回到do_fork,父进程醒来并返回
#include <sys/types.h> #include <stdio.h> #include <unistd.h> int glob = 6; int main(void) { int var; pid_t pid; var = 88; printf("before vfork\n"); if ((pid = vfork()) < 0) { fprintf(stderr, "vfork error\n"); } else if (pid == 0) { glob++; var++; _exit(0); } printf("pid=%d,glob=%d,var=%d\n", getpid(), glob, var); return 0; }
调用了_exit而不是exit。_exit并不执行IO缓存的刷新操作。如果是调用exit而不是_exit,则该程序的输出是:
可见父进程的printf输出消失了。其原因:子进程调用了exit,它刷新开关闭了所有标准IO流,这包括标准输出。虽然这是由子进程执行的,但却是在父进程的地址空间中进行的,所以所有受到影响的标准IO FILE对象都是在父进程中。当父进程调用prinf时,标准输出已经被关闭了,于是printf返回-1。
但是,在自己的linux系统上实验时,还是有print输出。
之所以结果不同是因为在linux中子进程关闭的是自己的, 虽然他们共享标准输入、标准输出、标准出错等 “打开的文件”, 子进程exit时,也不过是递减一个引用计数,不可能关闭父进程的,所以父进程还是有输出的。
线程的实现
内核并没有线程的概念,把所有的线程都当做进程来看,线程被看做与其他进程共享某些特定资源,每个线程都有属于自己的task_struct结构,他们共享描述符地址空间、打开的文件等资源,在创建线程时指定一些标志来共享他们的资源。
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
fork创建进程时:clone(SIGCHLD, 0);
vfork创建进程:clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);
内核线程与普通的线程区别是没有独立的地址空间(mm指针设为NULL),他们只在内核运行,从不切换到用户态去。
wait和waitpid
当一个进程正常或异常终止时会向父进程发送SIGCHLD信号。对于这种信号系统默认会忽略。调用wait/waidpid的进程可能会:
- 阻塞(如果其子进程都还在运行);
- 立即返回子进程的终止状态(如果一个子进程已经终止正等待父进程存取其终止状态);
- 出错立即返回(如果它没有任何子进程);(如果进程由于收到SIGCHLD信号而调用wait,则可期望wait会立即返回。但是在任一时刻调用则进程可能阻塞)
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *statloc); pid_t waitpid(pid_t pid, int *statloc, int options); //返回值: 成功返回进程ID, 出错-1.
这两个函数区别:
- wait如果在子进程终止前调用则会阻塞,而waitpid有一选项可以使调用者不阻塞。
- waitpid并不等待第一个终止的子进程--它有多个选项,可以控制它所等待的进程。
如果调用者阻塞而且它有多个子进程,则在其一个子进程终止时,wait就立即返回。因为wait返回子进程ID,所以调用者知道是哪个子进程终止了。
参数statloc是一个整型指针。如果statloc不是一个空指针,则终止状态就存放到它所指向的单元内。如果不关心终止状态则将statloc设为空指针。
这两个函数返回的整型状态由实现定义。其中某些位表示退出状态(正常退出),其他位则指示信号编号(异常返回),有一位指示是否产生了一个core文件等等。POSIX.1规定终止状态用定义在<sys/wait.h>中的各个宏来查看。有三个互斥的宏可用来取得进程终止的原因,它们的名字都已WIF开始。基于这三个宏中哪一个值是真,就可选用其他宏(这三个宏之外的其他宏)来取得终止状态、信号编号等。
wait是只要有一个子进程终止就返回,waitpid可以指定子进程等待。对于waitpid的pid参数:
- pid == -1, 等待任一子进程。这时waitpid与wait等效。
- pid > 0, 等待子进程ID为pid。
- pid == 0, 等待其组ID等于调用进程的组ID的任一子进程。
- pid < -1 等待其组ID等于pid的绝对值的任一子进程。
对于wait,其唯一的出错是没有子进程(函数调用被一个信号中断,也可能返回另一种出错)。对于waitpid, 如果指定的进程或进程组不存在,或者调用进程没有子进程都能出错。options参数使我们能进一步控制waitpid的操作。此参数或者是0,或者是下表中常数的逐位或运算。
- WCONTINUED:若实现支持作业控制,那么由pid指定的任一子进程在停止后已经继续,但其状态尚未报告,则返回其状态(POSIX.1的XSI扩展)
- WNOHANG:没有已终止的子进程时,则waitpid不阻塞。此时其返回值为0
- WUNTRACED:若某实现支持作业控制,而由pid指定的任一子进程已处于停止状态,并且其状态自停止依赖还未报告过,则返回其状态,WIFSTOPPED宏确定返回值是否对应于一个停止的子进程
waitid
#include <sys/wait.h> int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options); //返回值:成功返回0;出错,返回-1
- 与waitpid相似,waitid允许一个进程指定要等待的子进程,但它使用两个单独的参数表示要等待的子进程所属的类型,而不是将此与进程ID或进程组ID组合成一个参数。
- id参数的作用域idtype的值相关
idtype
P_PID | 等待一特定进程,id包含要等待子进程的进程ID |
P_PGID | 等待一特定进程组的任一子进程;id包含要等待子进程的进程组ID |
P_ALL | 等待任一子进程;忽略id |
options
下面各标志的按位或运算,指示调用者关注哪些状态变化
WCONTINUED、WEXITED、WSTOPPED这3个常量之一必须在options参数中指定
- WCONTINUED:等待一进程,它以前曾被停止,此后又已继续,但其状态尚未报告
- WEXITED:等待已退出的进程
- WNOHANG:如无可用的子进程退出状态,立即返回而非阻塞
- WNOWAIT:不破坏子进程退出状态。该子进程退出状态可由后续的wait、waitid、waitpid调用取得
- WSTOPPED:等待一进程,它已经停止,但其状态尚未报告
Linux 3.2.0、Mac OS X 10.6.8、Solaris 10支持waitid。Mac OS X 10.6.8并没有设置siginfo结构中的所有信息
wait3,wait4
#include <sys/types.h> #include <sys/wait.h> #include <sys/time.h> #include <sys/resource.h> pid_t wait3(int *statloc, int options, struct rusage *rusage); pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage); //返回值:成功返回进程ID。出错,返回-1
大多数UNIX系统提供了另外两个函数wait3和wait4。这两个函数是从UNIX系统的BSD分支延袭下来的。它们提供的功能比POSIX.函数wait和waitpid、waitod所提供的分别要多一个,这与附加参数rusage有关。该参数要求内核返回由终止进程及其所有子进程使用的资源概况
资源信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等
有关细节请参阅getrusage(2)手册页。这些资源信息只包括终止子进程,并不包括处于停止状态的子进程(这种资源信息与7.11节中所述的资源限制不同)
进程退出
进程调用exit后:
- 将struct_task中的成员设置为PF_EXITING
- 调用del_timer_sync删除内核任意一定时器;根据返回结果,他确保没有定时器在排队也没有定时器处理程序在运行
- 如果开启计账功能,则输出记账信息
- 调用exit_sem,如果进程在排队ipc则离开
- 调用_exit_files-递减文件描述符、_exit_fs-文件系统数据、exit_namesapce-进程名字空间、exit_sighand-信号处理函数的技术引用,若果其中某些值为0,则表明没有进程使用相应的资源,可以释放
- task_struct的exit_code设为exit的参数,退出码存放在这里供父进程使用
- 调用exit_notify向父进程发送信号,将子进程的父进程设为线程组中的其他线程或init进程并把状态设置为TASK_ZOMBIE
- do_exit调用schedule切换到其他进程,进程状态处于TASK_ZOMBIE不会再被调度
此时进程所占用的资源只有内核栈、thread_ifno、struct_task。为了子进程终止后后办法获取其信息,系统保留进程描述符,因此进程终止清理工作和进程描述符删除分开执行。
竞态调件
当多个进程都企图对某共享数据进行某种处理,而最后的结果又取决于进程运行的顺序,则我们认为这发生了竞态条件(race condition)。如果在fork之后的某种逻辑显式或隐式地依赖于在fork之后是父进程先运行还是子进程先运行,那么fork函数就会是竞态条件活跃的孽生地。
如果一个进程希望等待一个子进程终止,则它必须调用wait函数。如果一个进程要等待其父进程终止,则可使用下列形式的循环:
while(getppid() != 1) sleep(1);
这种形式的循环(称为定期询问(polling))的问题是它浪费了CPU时间,因为调用者每隔1秒都被唤醒,然后进行条件测试。
为了避免竞态条件和定期询问,在多个进程之间需要有某种形式的信号机制。在UNIX中可以使用信号机制,各种形式的进程间通信(IPC)也可使用。
在父、子进程的关系中,常常有以下情况:在fork之后,父、子进程都有一些事情要做。例如:父进程可能以子进程ID更新日志文件中的一个记录,而子进程则可能要为父进程创建一个文件。在本例中,要求每个进程在执行完它的一套初始化操作后要通知对方,并且在继续运行之前,要等待另一方完成其初始化操作。这种情况可以描述为如下:
TELL_WAIT(); if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { TELL_PARENT(getppid()); WAIT_PARENT(); exit(0); } TELL_CHILD(pid); WAIT_CHILD(); exit(0);
exec
当进程调用exec函数时,该进程完全由新进程代换,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID不会改变(父子进程这种关系也不会变)。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, const char *arg0, ... /* (char *)0, char *const envp[] */); int execve(const char *pathname, char *const argv[], char *const envp[]); int execlp(const char *filename, const char *arg0, ... /* (char *) 0 */); int execvp(const char *filename, char *const argv[]); //返回值:出错-1,若成功不返回
这些函数之间的第一个区别是前四个取路径名作为参数,后两个取文件名作为参数。当制定filename作为参数时:
- 如果filename中包含/,则就将其视为路径名。
- 否则按PATH环境变量。
如果excelp和execvp中的任意一个使用路径前缀中的一个找到了一个可执行文件,但是该文件不是机器可执行代码文件,则就认为该文件是一个shell脚本,于是试着调用/bin/sh,并以该filename作为shell的输入。
第二个区别与参数表的传递有关(l 表示表(list),v 表示矢量(vector))。函数execl、execlp和execle要求将新程序的每个命令行参数都说明为一个单独的参数。这种参数表以空指针结尾。另外三个函数execv,execvp,execve则应先构造一个指向个参数的指针数组,然后将该数组地址作为这三个函数的参数。
最后一个区别与向新程序传递环境表相关。以 e 结尾的两个函数excele和exceve可以传递一个指向环境字符串指针数组的指针。其他四个函数则使用调用进程中的environ变量为新程序复制现存的环境。
六个函数之间的区别:
每个系统对参数表和环境表的总长度都有一个限制。当使用shell的文件名扩充功能产生一个文件名表时,可能会收到此值的限制。例如,命令:
grep _POSIX_SOURCE /usr/include/*/*.h
在某些系统上可能产生下列形式的shell错误:arg list too long
执行exec后进程ID没改变。除此之外,执行新程序的进程还保持了原进程的下列特征:
- 进程ID和父进程ID。
- 实际用户ID和实际组ID。
- 添加组ID。
- 进程组ID。
- 对话期ID。
- 控制终端。
- 闹钟尚余留的时间。
- 当前工作目录。
- 根目录。
- 文件方式创建屏蔽字。
- 文件锁。
- 进程信号屏蔽。
- 未决信号。
- 资源限制。
- tms_utime,tms_stime,tms_cutime以及tms_ustime值。
对打开文件的处理与每个描述符的exec关闭标志值有关。进程中每个打开描述符都有一个exec关闭标志。若此标志设置,则在执行exec时关闭该文件描述符,否则该描述符仍打开。除非特地用fcntl设置了该标志,否则系统的默认操作是在exec后仍保持这种描述符打开。
POSIX.1明确要求在exec时关闭打开目录流。这通常是由opendir函数实现的,它调用fcntl函数为对应于打开目录流的描述符设置exec关闭标志。
在exec前后实际用户ID和实际组ID保持不变,而有效ID是否改变则取决于所执行程序的文件的设置-用户-ID位和设置-组-ID位是否设置。如果新程序的设置-用户-ID位已设置,则有效用户ID变成程序文件的所有者的ID,否则有效用户ID不变。对组ID的处理方式与此相同。
在很多UNIX实现中,这六个函数只有一个execve是系统调用。另外5个是库函数
更改用户id和组id
可以用setuid设置实际用户ID和有效用户ID。可以用setgid函数设置实际组ID和有效组ID。
#include <sys/types.h> #include <unistd.h> int setuid(uid_t uid); int setgid(gid_t gid); 返回值:成功为0,出错为-1
有关改变用户ID的规则。
- 若进程具有root特权,则setuid函数将实际用户ID、有效用户ID,以及保存的设置-用户-ID设置为uid。
- 若进程没有root权限,但是uid等于实际用户ID或保存的设置-用户-ID,则setuid只将有效用用户ID设置为uid。不改变实际用户ID和保存的设置-用户-ID。
- 如果上面两个条件都不满足,则errno设置为EPERM,并返回出错。
在这里假定_POSIX+_SAVED_IDS为真。如果没有提供这种功能,则上面所说的关于保存的设置-用户-ID部分都无效。
关于内核所维护的三个用户ID,还要注意以下:
- 只有root用户可以修改实际用户ID。通常,实际用户ID是在用户登录时,由login程序设置的,而且绝不会改变它。因为login进程是一个root进程,当它调用setuid时,设置所有三个用户ID。
- 仅当对程序文件设置了设置-用户-ID位时,exec函数设置有效用户ID。任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置-用户-ID。自然,不能将有效用户ID设置为任一随机值。
- 保存的设置-用户-ID是由exec从有效用户ID复制的。在exec按文件用户ID设置了有效用户ID后,即进行这种复制,并将此副本保存起来。
下表列出了改变这三个用户ID的不同方法
setreuid和setregid
4.3+BSD支持setregid函数,其功能是交换实际用户ID和有效用户ID的值。
#include <sys/types.h> #include <unistd.h> int setreuid(uid_t ruid, uid_t euid); int setregid(gid_t rgid, gid_t egid);
其作用是一个非特权用户总能交换实际用户ID和有效用户ID。这就允许一个设置-用户-ID程序转换成只具有用户的普通权限,以后又可再次切换回设置-用户-ID所得到大的额外权限。
seteuid和setegid
这两个函数只更改有效用户ID和有效组ID。
#include <sys/types.h> #include <unistd.h> int seteuid(uid_t uid); int setegid(gid_t gid); //返回值: 成功为0,出错为-1
一个非特权用户可将有效用户ID设置为其实际用户ID获取保存的设置-用户-ID。对于一个特权用户可将有效用户ID设置为uid。
组ID
以上所说明的一切都以类似方式适用于各个组ID,添加组ID不受setgid函数的影响。
解释器文件
解释器文件就是linxu中的shell脚本。这种文件是文本文件,其起始行的形式是:
#! pathname [optional-argument]
在感叹号和pathname之间的空格是可任选的。最常见的是以下列行开始:
#! /bin/sh
pathname通常是个绝对路径名,对它不进行什么特殊的处理(不适用PATH进行路径搜索)。
很多系统对解释器文件第一行有长度限制(32个字符)。这包括#!、pathname、可选参数以及空格数。
system
#include <stdlib.h> int system(const char *cmdstring);
如果cmdstring是一个空指针,则仅当命令处理程序可用时,system返回非0,这一特征可以确定在一个给定的操作系统上是否支持system函数。在UNIX中,system总是可用的。
因为system函数在实现中调用了fork、exec和waitpid,因此有3种返回值。
- fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,并且设置errno以指示错误类型。
- 如果exec失败,则其返回值如同shell执行了exit。 (3)否则所有3个函数都成功,那么system的返回值是shell的终止状态,其格式在waitpid中说明。
一下是system的一种实现:
#include <sys/wait.h> #include <errno.h> #include <unistd.h> #include <stdio.h> int system(const char *cmdstring) { pid_t pid; int status; if (NULL == cmdstring) { return 1; } if ((pid = fork()) < 0) { status = -1; } else if (pid == 0) { execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); //execl("/home/dang/WorkSpace/test","xxxxxx","yyyyyy",(char*)0); _exit(127); } else { while ((waitpid(pid, &status, 0)) < 0) { if (errno != EINTR) { status = -1; break; } } printf("parent process\n"); } return status; } int main(int argc, char **argv) { if (system("date > file")<0) { puts("cmd is failed"); } else { puts("cmd is success"); } puts("main done"); return 0; }
shell的-c选项告诉shell程序读取下一个命令行参数(在这里是cmdstring)作为命令输入。shell对以null字节终止的命令字符串进行语法分析,将他们分成命令行参数。传递给shell的实际命令字符串可以包括任一有效的shell命令。例如,可以用<和>岁输入和输出重定向。
首先在调用system函数时,如果出错,则掉用exit函数退出,如下代码测试为:
#include <stdio.h> #include <sys/wait.h> #include <stdlib.h> int main(void) { int status; if ((status = system("date")) < 0) { printf("system error!\n"); } exit(status); //pr_exit(status); if ((status = system("nosuchcommand")) < 0) { printf("system error!\n"); } exit(status); //pr_exit(status); if ((status = system("who;exit 44")) < 0) { printf("system error!\n"); } exit(status); //pr_exit(status); exit(0); }
在遇到第二个非法的system命令时,执行失败,进程退出。
在调用system函数时,如果出错,则调用pr_exit函数,打印出出错原因以及状态,以下为测试程序:
pr_exit函数的实现为:
#include <stdlib.h> #include <stdio.h> void pr_exit(int status) { if (WIFEXITED(status)) printf("normal termination,exit status = %d\n", WEXITSTATUS(status)); else if (WIFSIGNALED(status)) printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status), #ifdef WCOREDUMP WCOREDUMP(status) ? "(core file generated)" : ""); #else ""); #endif else if (WIFSTOPPED(status)) printf("child stopped, signal number = %d\n", WSTOPSIG(status)); }
将该函数编译成共享库形式,执行命令:
gcc -O -fpic -shared -o pr_exit.so pr_exit.c
在主函数中可以调用该函数,主函数为:
#include <stdio.h> #include <sys/wait.h> #include <stdlib.h> int main(void) { int status; if ((status = system("date")) < 0) { printf("system error!\n"); } pr_exit(status); if ((status = system("nosuchcommand")) < 0) { printf("system error!\n"); } pr_exit(status); if ((status = system("who;exit 44")) < 0) { printf("system error!\n"); } pr_exit(status); exit(0); }
编译时加上该命令
gcc -o systemTest systemTest.c ./pr_exit.so
使用system而不直接使用fork和exec的优点是:system函数进行了各种出错处理以及各种信号处理
进程会计
大多数UNIX系统提供了一个选项以进行进程会计处理。启用该选项后,每个进程结束时内核就写一个会计记录。典型的会计记录包含总量较小的二进制数据,一般包括命令名、所使用的CPU时间总量、用户和组ID、启动时间等。
会计记录结构定义在头文件
typedef u_short comp_t; struct acct { char ac_flag; char ac_stat; uid_t ac_uid; gid_t ac_gid; dev_t ac_tty; time_t ac_btime; comp_t ac_utime; comp_t ac_stime; comp_t ac_etime; comp_t ac_mem; comp_t ac_io; comp_t ac_rw; char ac_comm[8]; };
会计记录所需的各个数据(各CPU时间、传输的字节数等)都由内核保存早进程表中,并在一个新进程被创建时初始化。进程终止时写一个会计记录。这产生进程终止时写一个会计记录。这产生两个后果。
- 我们不能获取永远不终止的进程的会计记录。像init这样的进程在系统生命周期中一直在运行,并不产生会计记录。这也同样适合于内核守护进程,他们通常不会终止。
- 在会计文件中记录的顺序对应于进程终止的顺序,而不是他们启动的顺序。为了确定启动顺序,需要读全部会计文件,并按照日历时间进行排序。
会计记录对应于进程而不是程序。在fork之后,内核为子进程初始化一个记录,而不是在一个新程序被执行时初始化。虽然exec并不创建一个新的会计记录,但相应记录中的命令名改变了,AFORK标志则被清除。这意味着,如果一个进程顺序执行了3个程序(A exec B、B exec C,最后是C exit),只会写一个进程会计记录。在该记录中的命令名对应于程序C,但是CPU时间是程序A、B、C之和。
用户标识
任一进程都可以得到其实际用户ID和有效用户ID及组ID。但是,我们有时候希望找到运行该程序用户的登录名。我们可以调用getpwuid。但是如果一个用户有多个登录名,这些登录名又对应着同一个用户ID,又将如何呢?可以用getlogin函数可以获取登陆此登录名。
#include <unistd.h> char *getlogin(void); //返回值:若成功,返回指向登录名字符串的指针;若出错,返回NULL
如果调用此函数的进程没有连接到用户登录时所用的终端,则函数会失败。通常称这些进程为守护进程(daemon)。 给出了登录名,就可用getpwnam在口令文件中查找用户的相应记录,从而确定其登录shell等。
进程调度
进程可以通过调整nice值选择以更低优先级运行。只有特权进程允许提高调度权限。
nice值越小,优先级越高。NZERO是系统默认的nice值。
进程可以通过nice函数获取或者更改她的nice值。使用这个函数,进程只影响自己的nice值,不能影响任何其他进程的nice值。
#include <unistd.h> int nice(int incr); 返回值:若成功,返回信的nice值NZERO;若出错,返回-1
incr参数被增加到调用进程的nice值。如果incr太大,系统直接把他降到最大合法值,不给出提示。类似的,如果incr太小,系统也会无声息的把他提高到最小合法值。由于-1是合法的成功返回值,在调用nice函数之前需要清楚errno,在nice函数返回-1 时,需要检查他的值。如果nice调用成功,并且返回值为-1,那么errno任然为0.如果errno不为0,说明nice调用失败。
getpriority函数可以像nice函数那样用于获取进程的nice值,但是getpriority还可以获取一组相关进程的nice值。
#include <sys/resource.h> int getpriority(int which ,id_t who); //返回值:若成功,返回-NZERO~NZERO之间的nice值,若出错返回-1
which参数可以取下面三个值之一:PRIO_PROCESS表示进程,PRIO_PGRP表示进程组,PRIO_USER表示用户ID。which参数控制who参数是如何解释的,who参数选择感兴趣的一个或者多个进程。如果who参数为0,表示调用进程、进程组或者用户(取决于which参数的值)。当which设为PRIO_USER并who为0时,使用调用进程的实际用户ID。如果which参数作用于多个进程,则返回所有进程中优先级最高的。
setpriority函数可以用于为进程、进程组和属于特定用户ID的所有进程设置优先级
#include <sys/resource.h> int setpriority(int which, id_t who, int value); //返回值:若成功,返回0;若出错,返回-1
参数which和who与getpriority相同。value增加到NZERO上,然后变为新的nice值,以下的程序度量了调整nice值的效果。两个进程并行运行,各自增加自己的计数器。
#include <errno.h> #include <sys/time.h> #include <sys/param.h> #include <stdio.h> #include <stdlib.h> unsigned long long count; struct timeval end; void checktime(char *str) { struct timeval tv; gettimeofday(&tv, NULL); if (tv.tv_sec >= end.tv_sec && tv.tv_usec >= end.tv_usec) { printf("%s count = %llu\n", str,count); exit(0); } } int main(int argc, char **argv) { pid_t pid; char *s; int nzero, ret; int adj = 0; setbuf(stdout, NULL); #if defined(NZERO) nzero = NZERO; #elif defined(_SC_NZERO) nzero = sysconf(_SC_NZERO); #else nzero = 0; //#error NZERO undefined //编译器原因报错 #endif printf("NZERO = %d\n",nzero); if (argc == 2) adj = strtol(argv[1], NULL, 10); gettimeofday(&end, NULL); end.tv_sec += 10; if ((pid = fork()) < 0) { printf("fork error!\n"); } else if (pid == 0) { s = "child"; printf("current nice value in child is %d,adjusting by %d\n", nice(0) + nzero, adj) ; errno = 0; if ((ret = nice(adj)) == -1 && errno != 0) { printf("child set schduling priority\n"); printf("now child nice value in parent is %d\n", nice(0) + nzero); } } else { s = "parent"; printf("current nice value in parent is %d\n",nice(0) + nzero); } for (; ;) { if (++count == 0) { printf("%s counter wrap", s); } checktime(s); } }
进程时间
任一进程都可以调用times函数获取它自己以及终止子进程的墙上时钟时间、用户CPU时间和系统CPU时间。
#include <sys/times.h> clock_t times(struct tms *buf); //返回值:若成功,返回流逝的墙上时钟时间;若出错,返回-1
此函数填写由buf指向的tms结构,该结构定义如下:
struct tms { clock_t tms_utime; clock_t tms_stime; clock_t tms_cutime; clock_t tms_cstime; };
此结构没有包含墙上的时钟时间。times函数返回墙上时钟时间作为其函数值。此值是相对于过去的某一时刻度量的,所以不能用其绝对值而必须使用其相对值。例如,调用times,保存其返回值。在以后的某个时间再次调用times,从新返回的值减去以前返回的值,此差值就是墙上时钟时间。
所有由此函数返回的clock_t值都用_SC_CLK_TCK(由sysconf函数返回的每秒时钟滴答数)转换成秒数。
下面程序将每个命令行参数作为shell命令串执行,对每个命令计时,并打印从tms结构取得的值。
#include <sys/times.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> static void pr_times(clock_t, struct tms *, struct tms *); static void do_cmd(char *); static void pr_exit(int ); int main(int argc, char *argv[]) { int i; for (i = 1; i < argc; i++) do_cmd(argv[i]); /* once for each command-line arg */ exit(0); } static void do_cmd(char *cmd) /* execute and time the "cmd" */ { struct tms tmsstart, tmsend; clock_t start, end; int status; fprintf(stderr, "\ncommand: %s\n", cmd); if ( (start = times(&tmsstart)) == -1) /* starting values */ printf("times error"); if ( (status = system(cmd)) < 0) /* execute command */ printf("system() error"); if ( (end = times(&tmsend)) == -1) /* ending values */ printf("times error"); pr_times(end-start, &tmsstart, &tmsend); pr_exit(status); } static void pr_times(clock_t real, struct tms *tmsstart, struct tms *tmsend) { static long clktck = 0; if (clktck == 0) /* fetch clock ticks per second first time */ if ( (clktck = sysconf(_SC_CLK_TCK)) < 0) printf("sysconf error"); fprintf(stderr, " real: %7.2f\n", real / (double) clktck); fprintf(stderr, " user: %7.2f\n", (tmsend->tms_utime - tmsstart->tms_utime) / (double) clktck); fprintf(stderr, " sys: %7.2f\n", (tmsend->tms_stime - tmsstart->tms_stime) / (double) clktck); fprintf(stderr, " child user: %7.2f\n", (tmsend->tms_cutime - tmsstart->tms_cutime) / (double) clktck); fprintf(stderr, " child sys: %7.2f\n", (tmsend->tms_cstime - tmsstart->tms_cstime) / (double) clktck); } static void pr_exit(int status) { if (WIFEXITED(status)) printf("normal termination,exit status = %d\n", WEXITSTATUS(status)); else if (WIFSIGNALED(status)) printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status), #ifdef WCOREDUMP WCOREDUMP(status) ? "(core file generated)" : ""); #else ""); #endif else if (WIFSTOPPED(status)) printf("child stopped, signal number = %d\n", WSTOPSIG(status)); }
补充下线程的知识
Sleep本身就是可中断睡眠,sleep不是释放锁,wait释放锁