进程相关fork()/exec()/wait()
fork()
fork()将父进程复制一份子进程, 在子进程中从fork()调用处继续执行, 之后的代码在父子进程中各自执行一遍. 最终父进程的fork()返回子进程的pid, 子进程的fork()返回0表示创建成功. 所以看起来仿佛fork()返回两个返回值, 其实是两个进程的fork()各自的返回值, 通过返回值不同区分父子进程.
getpid()
获取当前进程pid, getppid()
获取父进程pid.
getuid()
获取当前进程实际用户, geteuid()
获取当前进程有效用户. 如使用sudo命令时shell进程有效用户就变为root, 但实际用户还是username.
循环创建子进程如下所示:
当pid==0即在子进程中时要跳出循环, 避免创建"孙进程". 使得只有主进程有调用fork
父子进程各自的代码段是独立的, 各自的全局变量也是独立的, 但对于只读操作可以共享同一块物理内存, 写时再复制, 虚拟地址空间还是独立的.
父子进程谁先执行并不一定.
gdb使用set follow-fork-mode child/parent
来跟踪父进程或子进程, 注意要在运行到 fork()前设置.
exec()函数族
调用exec函数会将当前进程的.text,.data段完全替换为新程序的.text和.data段, 但是不创建新进程, 所以进程id不变.
头文件<unistd.h>
extern char **environ;
原型:
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
execl()/execlp()/execle()
execl("/bin/ls", "ls", "-l", "-a", NULL)
直接传入文件路径, 不需要PATH环境变量, 第二个参数起是命令行参数argv[0], argv[1]..., 命令行参数也是一个字符串数组, 结尾的哨兵NULL要显式的写出来.
execlp("ls", "ls", "-l", "-a", NULL)
该函数需要配合PATH来搜索需要加载的程序. 其第一个参数ls指的是要从PATH中查找的程序; 第二个参数ls指的是argv[0], 表示要被调用的程序. 此处文件中不需要引入环境变量extern char** environ
, 函数会自动去查找. 其中environ是一个指针数组, 每一个指针指向一个char*字符串. execlp()通常用来调用系统程序如ls, date, cp, cat等命令.
先声明char* const envp[] = {"AA=11", "BB=22", NULL};
然后将envp传入execle("/bin/ls", "ls", "-l", "-a", NULL, envp)
. 原进程会将环境变量信息传递给新进程, 可以利用execle函数传递自己需要的环境变量信息.
execv()/execvp()/execve()
先声明char* argv[]={"ls", "-l", "-a", NULL};char* const envp[] = {"AA=11", "BB=22", NULL};
然后execv("/bin/ls", argv)
直接声明一个命令行参数数组, 再传进去. 这里的v指代argv. 同理, execvp("ls", argv)
需要借助PATH, execve("/bin/ls", argv, envp)
需要传入环境变量.
若想将ps -aux输出到屏幕的内容输出到一个文件中, 可以使用dup2(fd, STDOUT_FILENO)
将文件描述符表1号的STDOUT_FILENO指向对应文件的fd, 然后再execlp("ps", "ps", "aux", NULL)
.
注:1. 上述exec系列函数底层都是通过execve()系统调用实现, 2. exec函数族成功了不返回值, 失败了才返回errno, 所以后面的close(fd)实际上是不执行的, 不过依赖于隐式回收可以在进程结束时自动关闭文件, 所以不写close(fd)不会出错. 如果exec失败也不需要if判断, 直接perror接exit即可
孤儿进程
若父进程先于子进程结束, 则子进程成为孤儿进程, 其被/sbin/init进程领养(pid=1)或是/usr/sbin/init进程, 然后被回收.
僵尸进程
子进程死亡后父进程未处理, 则子进程成为僵尸进程, PCB仍存放在内核中. 使用ps aux查看时发现进程名变成[进程名]<defunct>, 表示是一个僵尸进程, 状态为Z. 另外运行中的进程状态是R, 后台运行的状态是S.
僵尸进程已经死亡, 无法用kill杀死, 所以只能回收. 回收一个僵尸进程可以调用wait()或者waitpid(), 也可以将其父进程杀死后使其变为孤儿进程, 由init领养后回收.
wait()
pid_t wait(int *status)
传出参数status(配合宏)表示僵尸进程的成因, 返回值为僵尸进程pid. wait()函数可以清除PCB残留信息, 使父进程阻塞等待子进程完成. 一次wait()调用只能回收一个子进程.
// 当子进程正常结束时(如return 13或者 exit(13)), 可以获取到退出信息13
if (WIFEXITED(status))
{
printf("child exit with %d", WEXITSTATUS(status));
}
// 当程序异常退出时(接收到信号), 获取中断信号(如9, kill -9)
// 当kill不加参数时信号默认为15-SIGTERM, 另外段错误是11, 可使用kill -l查看各种信号
if (WIFSIGNALED(status))
{
printf("child killed by %d", WTERMSIG(status));
}
if (WIFSTOPPED(status)
{
printf("child stopped by %d", WSTOPSIG(status));
}
if (WIFCONTINUED(status))
{
printf("child continued");
}
waitpid()
pid_t waitpid(pid_t pid, int* status, in options);
同wait, 但可以指定pid进程清理, 也可以不阻塞(options参数使用WNOHANG轮询模式), options为0则为阻塞.
fork的子进程默认跟父进程是一个进程组的, 所以如果父进程调用waitpid()时第一个参数传0和传-1是一样的. 父子进程组ID默认为父进程的ID
如果第一个参数传-xxxx就会把这一进程组的子进程都回收, 使用ps -ajx
可以查看到进程组ID, 等价于kill -9 -xxxx(进程组ID)
.
当第三个参数为0时, waitpid(-1, NULL, 0)等价于wait(NULL), 回收任意子进程. 第三个参数传WNOHANG时(需使用轮询结构), 非阻塞回收, 如果子进程正在运行函数返回0, 如果成功清理返回子进程ID, 如果失败(无子进程)返回-1.
进程组/会话
getpgrp()获取当前进程的进程组ID, getpgid获取指定进程的进程组ID(传0就是自身的). setpgid使某一进程自立门户, 成为新进程组的组长.
会话的SID是创建该会话的领头进程的PID, 一般就是shell. Session的意义在于多个进程组(job)在一个终端中运行,其中的一个为前台 job,它直接接收该终端的输入并把结果输出到该终端。其它的 job 则在后台运行。
如果我们在 session 中执行了 nohup 等类似的命令,当 session 消亡时,相关的进程并不会随着 session 结束,原因是这些进程不再受 SIGHUP 信号的影响.
nohup java -jar app.jar >log 2>&1 &
最后一个&表示把条命令放到后台执行, 2>&1一定要写到>log后面,才表示标准错误输出和标准输出都重定向到log中.
本来1----->屏幕 (1指向屏幕)
执行>log后, 1----->log (1指向log)
执行2>&1后, 2----->1 (2指向1,而1指向log,因此2也指向了log)
守护进程
创建守护进程: 1. 创建子进程, fork() 2. 子进程创建新会话,丢弃终端, setsid() 3. 移动工作目录到根目录, chdir() 4. 改变umask掩码, umask(0002) 5. 重定向012文件描述符到/dev/null(会话不需要终端), dup2() 6. 将1-5封装到一个函数中来调用, 之后开始执行守护进程任务