目录
(一)进程的基本问题
(1)什么是程序,什么是进程,有什么区别?
程序是指通过编译后再磁盘生成的拥有一定权限的文件,是静态概念,例如在终端中输入“gcc *.c -o pro”,生成的pro文件则被称为一个程序。进程是程序的一次运行活动,当程序运行时,会向系统申请内存,系统则多出一个进程,是动态概念。
(2)如何查看系统中有哪些进程?
- 使用ps指令查看,实际工作中,配合grep来查找程序中是否存在某一个进程。例如通过gcc编译生成的test文件。通过“ps -aux|grep test”搜寻相关进程,可以找到对应的进程编号3819.
#include <stdio.h>
int main(){
while(1){
}
return 0;
}
lamda@lamda-virtual-machine:~/Desktop/code/Linux/Pid$ gcc test.c -o test
lamda@lamda-virtual-machine:~/Desktop/code/Linux/Pid$ ./test
lamda@lamda-virtual-machine:~$ ps -aux|grep test
kernoops 933 0.0 0.0 11264 444 ? Ss 10:19 0:00 /usr/sbin/kerneloops --test
lamda 3819 100 0.0 2380 592 pts/2 R+ 13:43 1:13 ./test
lamda 3827 0.0 0.0 12120 720 pts/3 S+ 13:44 0:00 grep --color=auto test
- 使用top指令查看,类似windows任务管理器,top指令会动态查询当前进程,运行内存较大的会排在前面。
lamda@lamda-virtual-machine:~$ top
top - 13:50:22 up 3:31, 2 users, load average: 0.30, 0.22, 0.14
Tasks: 280 total, 2 running, 278 sleeping, 0 stopped, 0 zombie
%Cpu(s): 53.5 us, 0.3 sy, 0.0 ni, 46.2 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 3889.9 total, 1863.8 free, 1013.5 used, 1012.6 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 2616.3 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3829 lamda 20 0 2380 524 444 R 99.7 0.0 0:23.61 test
1882 lamda 20 0 4264832 395332 145940 S 3.7 9.9 1:16.56 gnome-shell
1741 lamda 20 0 307764 118692 63988 S 3.0 3.0 0:26.05 Xorg
3081 lamda 20 0 823712 55544 39940 S 1.3 1.4 0:07.74 gnome-terminal-
405 root -51 0 0 0 0 S 0.3 0.0 0:00.40 irq/16-vmwgfx
3437 root 20 0 0 0 0 I 0.3 0.0 0:02.82 kworker/1:1-events
3830 lamda 20 0 15072 4172 3356 R 0.3 0.1 0:00.14 top
1 root 20 0 169704 12972 8340 S 0.0 0.3 0:03.28 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.04 kthreadd
3 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 rcu_gp
4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 rcu_par_gp
5 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 slub_flushwq
6 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 netns
(3)什么是进程标识符?
每个进程都有一个非负整数表示的唯一ID,叫做pid,编程调用getpid函数获取自身的进程标识符,getppid获取父进程的进程标识符。
Pid=0: 称为交换进程(swapper),作用:进程调度
Pid=1:init进程,作用:系统初始化
(4)父进程,子进程,孤儿进程和僵尸进程?
- 如果进程A创建了进程B,那么A叫做父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系。
- 子进程退出状态不被收集,变成僵死进程(僵尸进程),查询进程状态会显示Z+。
- 父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫做孤儿进程。Linux避免系统存在过多孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程。(Ubuntu图形界面终端中由shell内核控制,所以在图形界面看到的init进程编号不一定是1,如果转换到字符界面,则编号会变为1)
(5)C程序的存储空间是如何分配?
《UNIX环境高级编程》书中指出,对于一个标准的C程序,系统会在编译后对其进行内存分配,地址由低到高依次是:
- 正文,预处理后系统将正文中的注释去掉,保存剩余的代码正文;
- 初始化的数据,该部分指代全局变量中已初始化的数据;
- 未初始化数据,全局变量中未初始化的数据,被称为bss(block started by symbol);
- 堆,函数中通常会申请数组或字符串等动态内存分配,一般都以堆的形式存放;
- 栈,函数中存放的其他数据形式;
- 命令行参数和环境变量,可直接与系统进行操作的命令。
当创建一个子进程后,子进程通常会复制父进程的全部代码变量,开辟一个进程进行操作,目前更流行使用写时复制操作(COW,copy on write)技术,这些区域由父进程和子进程共享,访问权限都是只读,当某一方试图修改这些区域,内核会只为修改该区域的内存制作副本。
(二)进程创建及收集退出状态
(1)使用fork创子进程
fork函数调用成功,返回两次。返回值为0,代表当前进程是子进程;返回值非负数,代表当前进程为父进程;调用失败,返回-1。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
pid_t pid;
pid = getpid();
printf("my code pid = %d\n",pid);
int num = 0;
pid = fork();
if(pid==0){
sleep(1);
num++;
printf("I think this is child pid, pid = %d,num = %d\n",getpid(),num);
}
else if(pid>0) {
wait(NULL);
printf("I think this is father pid, pid = %d,num = %d\n",getpid(),num);
}
else{
printf("fork error.\n");
}
return 0;
}
运行程序查看父子进程编号,发现调用fork函数后,后面的代码内容运行了两次,父子进程同时拥有fork函数之前的变量,但不共享,例如num,子进程中修改了num变量,父进程没有任何变化。
lamda@lamda-virtual-machine:~/Desktop/code/Linux/Pid$ gcc demo_fork.c -o fork
lamda@lamda-virtual-machine:~/Desktop/code/Linux/Pid$ ./fork
my code pid = 3927
I think this is child pid, pid = 3928,num = 1
I think this is father pid, pid = 3927,num = 0
(2)使用vfork创子进程
vfork与fork的区别在于,vfork 直接使用父进程存储空间,不拷贝;vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行。需要注意的是,如果子进程以break等非正常形式退出,则子进程共享的变量会被破坏掉。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(){
pid_t pid;
pid = getpid();
printf("my code pid = %d\n",pid);
int num = 0;
if(vfork()==0){
sleep(1);
num++;
printf("I think this is child pid, pid = %d,num = %d\n",getpid(),num);
exit(0);
}
else {
//wait(NULL);
printf("I think this is father pid, pid = %d,num = %d\n",getpid(),num);
}
return 0;
}
lamda@lamda-virtual-machine:~/Desktop/code/Linux/Pid$ ./vfork
my code pid = 4064
I think this is child pid, pid = 4065,num = 1
I think this is father pid, pid = 4064,num = 1
(3)收集退出状态
- 正常退出
Main函数调用return;
进程调用exit(),标准c库;
进程调用_exit()或者_Exit(),属于系统调用;
进程最后一个线程返回;
最后一个线程调用pthread_exit;
- 异常退出
调用abort;
当进程收到某些信号时,如ctrl+C;
最后一个线程对取消(cancellation)请求做出响应;
采用wait函数收集子进程退出状态:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(){
pid_t pid;
pid = getpid();
int status;
printf("my code pid = %d\n",pid);
int num = 0;
if(fork()==0){
sleep(1);
num++;
printf("I think this is child pid, pid = %d,num = %d\n",getpid(),num);
// exit(3);
return 3;
}
else {
wait(&status);
printf("I think this is father pid, pid = %d,num = %d\n",getpid(),num);
if(WIFEXITED(status))
printf("Child pid return num is %d\n",WEXITSTATUS(status));
else
perror("Why:");
}
return 0;
}
运行结果如下,return 和 exit 、_exit()、_Exit()函数退出都会被收集到:
lamda@lamda-virtual-machine:~/Desktop/code/Linux/Pid$ gcc demo_forkexit.c -o fork_exit
lamda@lamda-virtual-machine:~/Desktop/code/Linux/Pid$ ./fork_exit
my code pid = 4168
I think this is child pid, pid = 4169,num = 1
I think this is father pid, pid = 4168,num = 0
Child pid return num is 3
其中宏 WIFEXITED 和 WEXITSTATUS 都是用来解析 wait 函数收集的退出状态。
(4)等待子进程退出
一般创建子进程会需要新的进程完成一定操作后返回相关的数值,但父进程和子进程同时争夺运行资源,可能父进程在子进程还没结束时退出,则子进程会变成孤儿进程。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main(){
pid_t pid = fork();
if(pid > 0){
printf("PARENT : pid = %d, ppid = %d \n", getpid(), getppid());
}else if(pid == 0){
for(int i=0; i<3; i++){
printf("i = %d, pid = %d, ppid = %d \n", i, getpid(), getppid());
sleep(1);
}
exit(3);
}
return 0;
}
lamda@lamda-virtual-machine:~/Desktop/code/Linux/Pid$ ./guer
PARENT : pid = 4246, ppid = 3088
i = 0, pid = 4247, ppid = 4246
lamda@lamda-virtual-machine:~/Desktop/code/Linux/Pid$ i = 1, pid = 4247, ppid = 1652
i = 2, pid = 4247, ppid = 1652
这里为什么父进程结束后给子进程找到的新的父进程是1652,原因在于Ubuntu图形界面系统的终端仍然是shell控制,并不是直接由系统控制,如果转到字符界面则可以获得父进程为1。
参考博文:关于图形化界面详细解释
(三)进程中调用其他程序运行
(1)popen函数
有些情况下,进程的运行过程可能还会调用其他程序运行,常见的有exec族函数,system函数和popen函数,下面用popen函数调用ls命令,并将ls命令得到的数据存放到新的文件中。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
int main(){
pid_t pid;
pid = getpid();
printf("my code pid = %d\n",pid);
int status;
int num = 0;
if(fork()==0){
sleep(1);
printf("I think this is child pid, pid = %d,num = %d\n",getpid(),num);
printf(" I can use 'ls' command.\n");
//FILE *popen(const char *command, const char *type);
FILE *fps;
fps = popen("ls","r");
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
//size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
char readbuf[1024]={0};
int nread = fread(readbuf,sizeof(char),1024,fps);
pclose(fps);
fps = fopen("./ls.txt","w");
fwrite(readbuf,sizeof(char),strlen(readbuf),fps);
fclose(fps);
exit(0);
}
else {
wait(&status);
printf("I think this is father pid, pid = %d,num = %d\n",getpid(),num);
if(WIFEXITED(status))
printf("The status of child pid is %d\n",WEXITSTATUS(status));
}
return 0;
}
lamda@lamda-virtual-machine:~/Desktop/code/Linux/Pid$ ./popen
my code pid = 4307
I think this is child pid, pid = 4308,num = 0
I can use 'ls' command.
I think this is father pid, pid = 4307,num = 0
The status of child pid is 0
lamda@lamda-virtual-machine:~/Desktop/code/Linux/Pid$ cat ls.txt
a.out
demo4.c
demo_forkapply.c
demo_fork.c
demo_forkexit.c
demo_guer.c
demo_popen.c
demo_vfork.c
fork
fork_exit
guer
ls.txt
popen
test
test.c
vfork
(2)system函数
system函数调用十分简单,只需要将命令以字符串形式存放即可,但没有返回值,无法将所得数据进行保存。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
int main(){
pid_t pid;
pid = getpid();
printf("my code pid = %d\n",pid);
int status;
int num = 0;
if(fork()==0){
sleep(1);
printf("I think this is child pid, pid = %d,num = %d\n",getpid(),num);
printf(" I can use 'ls' command.\n");
system("ls");
exit(0);
}
else {
wait(&status);
printf("I think this is father pid, pid = %d,num = %d\n",getpid(),num);
if(WIFEXITED(status))
printf("The status of child pid is %d\n",WEXITSTATUS(status));
}
return 0;
}
lamda@lamda-virtual-machine:~/Desktop/code/Linux/Pid$ gcc demo_system.c -o system
lamda@lamda-virtual-machine:~/Desktop/code/Linux/Pid$ ./system
my code pid = 4446
I think this is child pid, pid = 4447,num = 0
I can use 'ls' command.
a.out demo_forkapply.c demo_forkexit.c demo_popen.c demo_vfork.c fork_exit ls.txt system test.c
demo4.c demo_fork.c demo_guer.c demo_system.c fork guer popen test vfork
I think this is father pid, pid = 4446,num = 0
The status of child pid is 0
(3)exec族函数
参考博文:exec族函数介绍
函数族:
exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe
函数原型:
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[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
返回值:
exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。
同样可以调用其它程序进行操控,具体使用方法参考上述博文,但一般情况下,使用system函数更为简洁方便。