进程(Process)和线程(Thread)
**相同点:**二者都是操作系统的任务,都会参与时间片轮转,都有5种状态
不同点:
**1.**线程不能独立存在,只能隶属于创建它的进程。因此,进程其实是线程容器
**2.**进程是系统分配资源的基本单位
每个进程都有独立内存四区和描述符数组
而线程只有其独立运行必须的栈区,其它三区和描述符数组共用隶属的进程的
**3.**线程是参与时间片轮转的最小单位
进程(通过其主线程)和线程都会参与时间片轮转,但线程的体量更小,因此线程被称轻量级任务,进程被称重量级任务
4. 多进程比多线程更为安全
一个进程崩溃不会导致其它进程的崩溃
一个线程崩溃会导致其隶属的进程崩溃
**5.**线程间通信比进程间通信更为便利
进程间通信需要借助于系统提供的一些专门机制进行,而同属一个进程的多线程间可以借助于一块内存空间就能实现通信
进程是至少拥有一个主线程的线程容器,它是操作系统分配资源的基本单位、它通过主线程参与时间轮转,一个进程崩溃不会导致其它进程崩溃,进程间通信只能借助于操作系统提供的相关机制进行
一、进程的管理
Linux操作系统采用多种数据结构来管理众多的进程
其中一种是树形结构
除了祖先进程,其它进程都是由其父进程调用相关函数创建。
因此,系统在任何一个时刻,同时运行的所有进程组成一个家谱
当一个进程的父进程先退出,则该进程将会成为孤儿进程,但系统不会让其一直脱离进程树的管控,会让进程树中某个进程领养这个进程
Linux 系统采用如下方式处理孤儿进程:让祖先进程领养所有成为孤儿的进程
每个进程都有一个唯一的身份标识,称为 pid
进程代码中可以通过调用 getpid函数获取自身的身份标识
可以通过调用 getppid函数获取自身的身份标识
二、进程的退出
进程在以下情况正常退出:
- 进程的 main函数返回
- 进程中执行到 exit函数调用语句 ---- 中途安全退出
- 进程中执行到 _exit函数调用语句 ---- 中途安全退出
- 最后一个线程退出
- 最后一个线程执行了 pthread_exit函数调用语句
进程异常退出情况:
- 进程执行到 abort函数调用语句 ---- 中途异常退出
- 进程发生段错误
- 最后一个线程响应取消请求
实际开发过程中,应该需要保证进程正常退出,并建议采用main函数返回形式或最后一个线程退出两种形式退出整个进程
任何一个进程退出(无论是哪种方式),都不会立即消亡,而是成为僵尸态,直到其父进程或系统对它做完善后处理才会彻底消亡
处于僵尸态的进程被称为僵尸进程
显然僵尸进程是一种内存泄露错误,编程时需要对已退出进程立即最善后处理
三、进程的创建
一个进程中可以通过调用fork 函数创建一个子进程
总结:
-
fork前只有父进程运行
-
fork 成功后,父子进程除了存放fork返回值的变量值不同,其它内存空间以及描述符数组双方内容一致
但除了代码区双方共享,其余三区和描述符数组各自用各自的(均为两份,一份给父进程用,另一份给子进程用)
-
在父进程中 fork 返回子进程的 pid ,在子进程中 fork 返回0,利用这一点可以决定后续代码哪些仅父进程执行,哪些仅子进程执行,哪些是双方都需要执行
进程相关命令:查看系统所有当前运行的进程
ps -aux :查看所有进程的 pid 、资源占用情况、状态(R ---运行态或就绪态 S ----睡眠态 Z----僵尸态)
ps -ef :查看所有进程的 pid 和 ppid
fork 前 open一个文件后的操作情况:
父子进程共用同一个引擎对象操作同一个文件(父子进程共用同一个位置指示器)
fork 后父子进程open同一个文件的操作情况
四、进程的善后
一个进程可能会有多个子进程,任何一个子进程退出,如果父进程不对该退出子进程进行及时的善后处理,就会导致该子进程长期处于僵死态。这种长期处于僵死态的进程被称为僵尸进程
僵尸进程已没有代码被执行,但仍然占用着一些系统资源,如果不及时(设延后t长时间)对其进行善后处理,则t长时间内这些被占用的系统资源无法被再度利用,造成t长时间的资源浪费。
父进程可以通过调用wait 或 waitpid 函数对已退出的子进程进行善后处理
可以使用带参宏 WEXITSTATUS (整数退出信息)中获取子进程 main 函数的返回值
如果不想获取已退出子进程的退出信息,形参wstatus 可以被传NULL
实际项目中,不能使用延时手段来让父子进程的代码段谁先运行谁后运行,但可以借助于wait 函数来实现
原因是 fork后父子进程是同时运行的,父进程的某行代码 与 子进程的某行代码谁先执行是由时间片轮转机制决定的,应用程序员无法明确!
但wait函数只能实现子进程先运行完后父进程再执行的顺序逻辑,想要设计出其它的顺序逻辑需要借助并发控制机制实现
系统提供 wait 函数的目的是为了避免僵尸进程,但有时候父进程可能会无法使用wait 函数来对已退出子进程及时进行善后,例如:
因此,wait函数也不能处理任意情况的子进程及时善后,避免僵尸进程的方法:
-
父进程调用 wait 函数
-
让父进程创建子进程后,子进程里再创建孙子进程,而子进程生完孙子立即退出,孙子进程成为孤儿进程。然后会被祖先进程领养,以后孙子进程退出善后就由祖先进程负责
cpp
pid_t cpid;
pid_t gpid;
cpid = fork();
if(cpid == 0){
gpid = fork();
if(gpid == 0){
//doing something
}
else{
exit(0);
}
}
else{
wait(NULL);//善后子进程
//。。。。。。。
}
- 在父进程 fork 前,调用 signal(SIGCHLD,SIG_IGN); ---- 通知系统,本进程的所有子进程退出善后由系统自动进行
练习: 编写程序完成如下功能,父进程创建子进程前动态分配一块大小为20 的内存空间,子进程向动态空间中拷贝字符串**"hello"** ,打印后退出,父进程等待子进程退出后,向动态空间拷贝字符串**"world"**,打印后退出
代码:
cpp
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc,char *argv[]){
pid_t pid;
char *pbuf = NULL;
//仅父进程在运行
pbuf = malloc(20);
pid = fork(); // fork一个子进程
if(pid < 0){
printf("fork child-process failed\n");
return 1;
}
if(pid > 0){//仅父进程执行
printf("pid=%d,aaaaaaa\n",getpid());
strcpy(pbuf,"hello");
}
else{//仅子进程执行
printf("pid=%d,bbbbbbb\n",getpid());
strcpy(pbuf,"world");
}
//父、子进程都需执行的代码
printf("pbuf = %s\n",pbuf);
free(pbuf);
pbuf = NULL;
return 0;
}
输出:
五、进程的替换
fork 子进程的目的:
为了让父子进程共用同一份代码,做相同或不同的事务 ------ 网络编程的服务端常用
为了让子进程去执行一个新可执行文件代表的程序 ------ 进程的替换
execl
execlp
execv
execvp
之间课程里说main函数argv[0]指向空间内容是可执行文件名本身,那是shell命令行程序默认形式,自己在代码中调用exec无需遵循shell命令行的规定
六、system函数
七、精灵进程或守护进程 daemon-process
满足如下特征的进程:
-
没有控制终端---无法在屏幕上打印信息也无法接收键盘输入
-
后台执行
让自己的程序后台执行:./test &
让自己的程序前台执行:./test
-
系统自举时启动,系统关闭时终止
-
进程组的组长进程以及会话的首进程,也是唯一进程
进程组:一个或多个进程的集合,组长进程是指创建进程组的进程,组长进程的pid就是进程组的组id
会话:一个或多个进程组的集合,会话首进程是指创建会话的进程
-
父进程是祖先进程
-
以超级用户身份特权运行
调用如下函数可以让自己编写的程序运行起来后成为守护进程
cpp#include <stdio.h> #include <stdlib.h> #include <syslog.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/resource.h> #include <unistd.h> #include <signal.h> void daemonize(const char *cmd) { int i, fd0, fd1, fd2; pid_t pid; struct rlimit rl; struct sigaction sa; /* * Clear file creation mask. */ umask(0); /* * Get maximum number of file descriptors. */ if (getrlimit(RLIMIT_NOFILE, &rl) < 0) printf("%s: can't get file limit", cmd); /* * Become a session leader to lose controlling TTY. */ if ((pid = fork()) < 0) printf("%s: can't fork", cmd); else if (pid != 0) /* parent */ exit(0); setsid(); /* * Ensure future opens won't allocate controlling TTYs. * 通过再次创建子进程结束当前进程,使进程不再是会话首进程来禁止进程重新打开控制终端 */ sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGHUP, &sa, NULL) < 0) printf("can't ignore SIGHUP"); if ((pid = fork()) < 0) printf("%s: can't fork", cmd); else if (pid != 0) /* parent */ exit(0); /* * Change the current working directory to the root so * we won't prevent file systems from being unmounted. */ if (chdir("/") < 0) printf("can't change directory to /"); /* * Close all open file descriptors. */ if (rl.rlim_max == RLIM_INFINITY) rl.rlim_max = 1024; for (i = 0; i < rl.rlim_max; i++) close(i); /* * Attach file descriptors 0, 1, and 2 to /dev/null. */ fd0 = open("/dev/null", O_RDWR); fd1 = dup(0); fd2 = dup(0); /* * Initialize the log file. */ openlog(cmd, LOG_CONS, LOG_DAEMON); if (fd0 != 0 || fd1 != 1 || fd2 != 2) { syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2); exit(1); } }