和文件描述符类似,每个进程都有一个非负数的唯一ID来表示它。进程ID可以在不同时刻复用,当一个进程终止后,它的ID就可以复用了。UNIX系统通常会有一个延迟的复用算法,使得新创建的进程ID不同于最近一段时间内终止的进程ID,以避免将新进程误认为是之前已终止的那个进程。
进程ID为1的通常是init进程,它在系统自举结束后由内核创建,该进程是用来初始化系统的,它通常会读取系统初始化的一些配置文件,将系统状态引导至初始化状态,init进程是不可能终止的。它是一个root用户进程,而不是内核进程,因此不是内核的一部分。
内存中的每个进程,都会在/proc目录下建立一个目录,目录名就是进程的ID(PID)。若进程结束,则目录消失。cat命令查看/proc/进程ID/maps就可以看到进程的内存分配情况。
除了进程ID,每个进程还有一些其他属性(如进程的父进程),函数原型如下:
#include <unistd.h> pid_t getpid(void); // 返回值:进程ID pid_t getppid(void); // 返回值:进程的父进程ID pid_t getuid(void); // 返回值:进程的实际用户ID pid_t geteuid(void); // 返回值:进程的有效用户ID pid_t getgid(void); // 返回值:进程的实际组ID pid_t getegid(void); // 返回值:进程的有效组ID
上述函数没有出错返回。
在命令行中使用命令ps -ef或ps-aux可以查看系统所有进程,如下图:
我们可以看到进程状态那一栏有各种字母,这些字母意味着什么呢?
S:休眠状态,进程大多数处于休眠状态;
s:说明该进程有子进程(父进程);
R:正在运行的进程;
Z:僵尸进程(已经结束但资源没有回收的进程),这种进程是很危险的,应该避免。
关于父进程和子进程:如果进程a启动了进程b,a叫b的父进程,b叫a的子进程。
fork()函数原型如下:
#include <unistd.h> pid_t fork(void);
此函数会返回两次,父进程返回子进程的PID,子进程会返回0;失败返回-1。
此函数有以下几个重点需要我们特别注意:
1. fork()是通过复制父进程的内存空间创建子进程,复制除了代码区以外的所有区域,代码区父子进程共享(因为代码区是只读的);
2. fork()会创建一个子进程,子进程从fork()当前位置开始执行代码,fork()之前的代码父进程执行一次,fork()之后的代码父子进程分别执行一次(共两次);
3. fork()创建子进程时,如果父进程有文件描述符,子进程会复制文件描述符,不复制文件表(父子进程共用一个文件表);
4. fork()创建子进程后,父子进程谁先运行不确定,谁先结束也不确定。
下面我们来看一个示例代码:
1 #include <stdio.h> 2 #include <unistd.h> 3 4 int g_a = 222; 5 6 int main() 7 { 8 int b = 111; 9 pid_t pid; 10 11 if (pid = fork()) /* 父进程 */ { 12 printf("PID: %d\n", getpid()); 13 14 --g_a; 15 --b; 16 printf("g_a = %d, b = %d\n", g_a, b); 17 printf("&g_a = %p, &b = %p\n", &g_a, &b); 18 19 sleep(1); /* 让父进程比子进程晚执行完成 */ 20 } 21 else /* 子进程 */ { 22 printf("Father PID: %d, Son PID: %d\n", getppid(), getpid()); 23 24 ++g_a; 25 ++b; 26 printf("g_a = %d, b = %d\n", g_a, b); 27 printf("&g_a = %p, &b = %p\n", &g_a, &b); 28 } 29 30 return 0; 31 }
此代码执行结果如下图:
通过结果可以发现,父子进程使用相同的虚拟地址,而且两进程的变量值又互不影响。这是由于父子进程把相同的虚拟地址映射到不同的物理地址。
对于上述第三个重点,我在此借用UNIX环境高级编程中的图片。子进程只复制了父进程的fd,但和父进程共用一个文件表,也就是共享文件偏移量、文件权限等。其实质类似于dup()类函数。
fork()函数产生子进程之后,子进程可以使用exec()类函数来执行新的程序,exec之后新的进程仍然和父进程共享同一个文件描述符。
在exec后,文件描述符默认保持打开状态。除非显式使用fcntl()设置,或者在open()打开文件时显式指定。
进程能正常执行结束退出,也可能未执行完毕异常终止。
第一种情况下,exit()类函数会获得一个进程main函数的return返回值作为“退出状态”,然后内核将“退出状态”转换为“终止状态”;
第二种情况下,内核为其产生一个“终止状态”。
这两种情况产生的“终止状态”中含有该进程相关的一些信息,比如进程ID、进程终止状态、进程CPU使用情况、有无core dump等信息。其实就是进程的资源仍然存在,需要使用wait()类函数来对其处理。
当子进程终止时,内核会向它的父进程发送一个SIGCHLD信号,该信号是一个异步事件。父进程可以忽略该信号或者加以捕捉处理。wait()和waitpid()可以让父进程等待子进程的结束,并取得子进程的退出状态和退出码(return后面的值或exit中的值)。其函数声明如下:
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options);
两函数成功返回处理进程的PID,出错则返回-1。
waitpid()函数出错返回值还有可能是0,此种情况是:当设置了WNOHANG选项(不阻塞等待),并且所要处理的子进程存在,但尚未有子进程需要处理,则返回0,如果没有子进程符合,或者没有子进程,则返回-1。
wait()函数等待任意子进程结束后返回;
waitpid()函数可以等待指定子进程结束后返回,其参数pid用于指定等待哪个进程,取值如下:
== -1,等待任意子进程,与wait()等效
> 0,等待指定子进程(指定pid)
== 0,等待本进程组的任一子进程
< -1,等待进程组ID等于pid绝对值的任意子进程
宏函数WIFEXITED(status)可以判断是否正常退出,WEXITSTATUS(status)可以取到退出码。示例代码如下:
1 /* wait()等待 */ 2 int status; 3 pid_t pid = wait(&status); // pid是子进程ID 4 if(WIFEXITED(status)) /* 阻塞等待子进程结束 */ { 5 printf("返回码%d\n", WEXITSTATUS(STATUS)); 6 } 7 8 /* waitpid()等待 */ 9 int status; 10 pid_t wpid = waitpid(-1/*pid*/, &status, 0); 11 /* 在此处-1表示等到了子进程就退出,pid表示等待到了名为pid的子进程就退出 */ 12 13 if (WIFEXITED(status)) /* 子进程是否正常结束 */ { 14 printf("等到了%d子进程,退出码:%d\n", wpid, WEXITSTATUS(status)); 15 }
多个进程对同一个文件读写时,就可能出现读写顺序竞争的问题,对数据的读写取决于进程的访问顺序,这就构成了竞争条件。为了避免竞争,就需要进行进程同步。
同步方式有原子操作,互斥量等,其使用方式可以查看:五、并发控制
如果fork()之后要执行全新的程序,需要使用exec()函数来加载。exec()函数加载的新程序会从main()函数开始重新执行,并且清空之前进程复制的代码段、数据段、堆、栈,但进程的ID不变。有7种不同的exec函数,它们被统称为exec函数,其函数声明如下:
#include <unistd.h> int execl(const char *path, const char *arg, ... /* (char *) NULL */); int execlp(const char *file, const char *arg, ... /* (char *) NULL */); int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]); int fexecve(int fd, char *const argv[], char *const envp[]);
上述函数失败返回-1,成功则不返回。
在此只解释execl()函数,其余函数和execl()函数差别不大。
execl()使用方式如下:
execl("程序的路径", "执行命令", "选项", "参数", "NULL");
只有第一个参数是必须正确的,第二个参数必须存在但可以不正确,第三个和第四个参数可以没有,NULL表示参数结束了。使用示例如下:
execl("./b.out", "b.out", NULL);
示例代码如下:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 #include <sys/wait.h> 5 6 char *const ps_argv[] = {"ps", "-o", "pid,ppid,pgrp,comm", NULL}; 7 8 int main() 9 { 10 pid_t pid; 11 12 pid = fork(); 13 if (pid == -1) { 14 perror("fork"); 15 return 1; 16 } 17 18 if (pid == 0) { 19 // 加载新映像 20 //execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,comm", NULL); 21 22 //execlp("ps", "ps", "-o", "pid,ppid,pgrp,comm", NULL); 23 execvp("ps",ps_argv); 24 } 25 else { 26 wait(NULL); 27 } 28 29 return 0; 30 }
vfork()和fork()在语法上没有区别,唯一区别在于vfork()不复制父进程的任何资源,而是直接占用父进程的资源运行代码,从而使子进程线运行,父进程处于阻塞状态,直到子进程结束或者调用了exec()系列函数。
需要注意的是,vfork()如果占用的是父进程的资源,必须用exit()显式退出。
vfork()和execl()的合作方式:
vfork()可以创建新的进程,但没有代码和数据;execl()创建不了新进程,但可以为进程提供代码和数据。
system()函数
通过system()可以能够使用shell来执行命令,相当于用C/C++程序来调用shell。其函数声明如下:
#include <stdlib.h> int system(const char* cmdstring);
该函数失败返回值较多,具体需参考说明手册
进程调度
UNIX系统提供了一个API接口,可以用来粗略调整进程运行优先级。其函数声明如下:
#include <unistd.h> int nice(int inc);
该函数成功返回(nice - NZERO),失败则返回-1。
由于(nice - NZERO)可能为负值,因此对于-1的返回值,需要判断errno。如果nice参数太大,进程优先级会调整到上限;如果nice参数太小,进程优先级会调整到下限;两者都不会给出任何提示,都是静默行为。
下一章 第十章:信号
原文:https://www.cnblogs.com/Lioker/p/10854946.html