1. Linux进程概述
进程是一个程序一次执行的过程,它和程序有本质区别。
程序是静态的,它是一些保存在磁盘上的指令的有序集合;而进程是一个动态的概念,它是一个运行着的程序,包含了进程的动态创建、调度和消亡的过程,是Linux的基本调度单位。
那么从系统的角度看如何描述并表示它的变化呢?在这里,是通过进程控制块(PCB)来描述的。进程控制块包含了进程的描述信息、控制信息以及资源信息,它是进程的一个静态描述。
内核使用进程来控制对CPU和其他系统资源的访问,并且使用进程来决定在CPU上运行哪个程序,运行多久以及采用什么特性运行它。内核的调度器负责在所有的进程间分配CPU执行时间,称为时间片(time slice),它轮流在每个进程分得的时间片用完后从进程那里抢回控制权。
1.1. 进程标识
OS会为每个进程分配一个唯一的整型ID,做为进程的标识号(pid)。进程除了自身的ID外,还有父进程ID(ppid),所有进程的祖先进程是同一个进程,它叫做init进程,ID为1,init进程是内核自举后的一个启动的进程。init进程负责引导系统、启动守护(后台)进程并且运行必要的程序。
进程的pid和ppid可以分别通过函数getpid()和getppid()获得。
1.2. 进程的用户ID与组ID(进程的运行身份)
进程在运行过程中,必须具有一类似于用户的身份,以便进行进程的权限控制,缺省情况下,哪个登录用户运行程序,该程序进程就具有该用户的身份。例如,假设当前登录用户为gotter,他运行了ls程序,则ls在运行过程中就具有gotter的身份,该ls进程的用户ID和组ID分别为gotter和gotter所属的组。这类型的ID叫做进程的真实用户ID和真实组ID。真实用户ID和真实组ID可以通过函数getuid()和getgid()获得。
与真实ID对应,进程还具有有效用户ID和有效组ID的属性,内核对进程的访问权限检查时,它检查的是进程的有效用户ID和有效组ID,而不是真实用户ID和真实组ID。缺省情况下,用户的(有效用户ID和有效组ID)与(真实用户ID和真实组ID)是相同的。有效用户id和有效组id通过函数geteuid()和getegid()获得。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 int main() 5 { 6 printf("uid:%d gid:%d euid:%d egid:%d ",getuid(),getgid(),geteuid(),getegid()); 7 return 0; 8 }
运行结果:
uid=500(ghaha) gid=500(ghaha) groups=500(ghaha)
编译生成可执行文件a.out,程序文件的属性可能为:
-rwxrwxr-x 1 ghaha ghaha 12132 Oct 7 09:26 a.out
执行结果可能为:
shell>./a.out
uid:500 gid:500 euid:500 egid:500
现在将a.out的所有者可执行属性改为s
shell>chmod u+s a.out
shell>ls -l
-rwsrwxr-x 1 ghaha ghaha 12132 Oct 7 09:26 a.out
此时改另外一个用户gotter登录并运行程序a.out
shell>id
uid=502(gotter) gid=502(gotter) groups=502(gotter)
shell>./a.out
uid:502 gid:502 euid:500 egid:502
可以看到,进程的有效用户身份变为了ghaha,而不是gotter了,这是因为文件a.out的访问权限的所有者可执行为设置了s的属性,设置了该属性以后,用户运行a.out时,a.out进程的有效用户身份将不再是运行a.out的用户,而是a.out文件的所有者。
s权限最常见的例子是
/usr/bin/passwd程序,它的权限位为
shell>ls /usr/bin/passwd
-r-s--x--x 1 root root 16336 Feb 13 2003 /usr/bin/passwd
我们知道,用户的用户名和密码是保存在/etc/passwd(后来专门将密码保存在/etc/shadow,它是根据/etc/passwd文件来生成/etc/shadow的,它把所有口令从/etc/passwd中移到了/etc/shadow中。这里用到的是影子口令,它将口令文件分成两部分:/etc/passwd和/etc/shadow,此时/etc/shadow就是影子口令文件,它保存的是加密的口令,而/etc/passwd中的密码全部变成x)下的。通过ls –l查看/etc/passwd这个文件,你会发现,这个文件普通用户都没有可写的权限,那我们执行passwd的时候确实能够修改密码,那么这是怎么回事呢?也就是说,任何一个用户运行该程序时,该程序的有效身份都将是root(用普通身份去执行这个操作的时候,它会暂时得到文件拥有者root的权限),而这样passwd程序才有权限读取/etc/passwd文件的信息。
我们也来实现以下passwd的功能,实现步骤如下:
1. 用touch创建一个1.txt输入内容“hello” 类似于/etc/passwd
2. 写一个程序3.c如下:
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<unistd.h> 4 #include<sys/types.h> 5 #include<errno.h> 6 7 int main() 8 { 9 printf("uid = %d gid = %d ",getuid(),getgid()); 10 printf("euid = %d egid = %d ",geteuid(),getegid()); 11 12 FILE *fp = fopen("1.txt","a"); 13 if(NULL == fp) 14 { 15 printf("fopen error! "); 16 exit(-1); 17 } 18 fputs("world ",fp); 19 fclose(fp); 20 return 0; 21 }
3. 编译gcc –o 1 1.c生成可执行程序1 此时1类似于/usr/bin/passwd文件
4. 用ll查看a.txt发现权限是-rw-r—r—的权限(跟/etc/passwd权限一样),说明普通用户没有可写权限,如果直接切换到普通用户执行./1会报错。
5. 用root身份将1的权限修改为-rwsr-xr-x (操作命令:chmod u+s 1,此时跟/usr/bin/passwd的权限一样),此时再切换到普通用户wangxiao,执行./1发现可以执行成功,因为此时./1进程的有效用户id变成root了,也就是说普通用户是借助root身份来实现的。
1.3. 进程的状态
进程是程序的执行过程,根据它的生命周期可以划分成3种状态。
1执行态:该进程正在运行,即进程正在占用CPU。
2就绪态:进程已经具备执行的一切条件,正在等待分配CPU的处理时间片。
3等待态:进程不能使用CPU,若等待事件发生(等待的资源分配到)则可将其唤醒。
1.4. Linux下的进程结构
Linux系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。也就是说,进程之间是分离的任务,拥有各自的权利和责任。其中,每个进程都运行在各自独立的虚拟地址空间,因此,即使一个进程发生了异常,它也不会影响到系统的其他进程。
Linux中的进程包含3个段,分别为“数据段”、“代码段”和“堆栈段”。
- “数据段”放全局变量、常数以及动态数据分配的数据空间。数据段分成普通数据段(包括可读可写/只读数据段,存放静态初始化的全局变量或常量)、BSS数据段(存放未初始化的全局变量)以及堆(存放动态分配的数据)。
- “代码段”存放的是程序代码的数据。
- “堆栈段”存放的是子程序的返回地址、子程序的参数以及程序的局部变量等。
1.5. Linux下的进程管理
启动进程:手工启动 调度启动
备注:
进程process:是os的最小单元 os会为每个进程分配大小为4g的虚拟内存空间,其中 1g给内核空间 3g给用户空间{代码区 数据区 堆栈区}
ps查看活动进程
ps –aux查看所有的进程
ps -aux| grep 'aa'查找指定(aa)进程
ps –ef可以显示父子进程关系 top显示前20条进程,动态的改变
pgrep 'vi'查找进程
进程状态:执行 就绪 等待状态
ps -aux看%cpu(cpu使用量) %mem(内存使用量) stat状态{S睡眠 T暂停 R运行 Z僵尸}
vi a.c &(&表示后台运行),一个死循环,按ctrl+z可以把进程暂停,再执行[bg作业ID]可以将该进程带入后台。利用jobs可以查看后台任务,fg 1把后台任务带到前台,这里的1表示作业ID
kill -9 进程号表示向某个进程发送9号信号,从而杀掉某个进程 利用pkill a可以杀死进程名为a的进程
2. 进程的创建
Linux下有四类创建子进程的函数:system(),fork(),exec*(),popen()
2.1. system函数
原型:
#include <stdlib.h>
int system(const char *string);
system函数通过调用shell程序/bin/sh –c来执行string所指定的命令,该函数在内部是通过调用execve(“/bin/sh”,..)函数来实现的。通过system创建子进程后,原进程和子进程各自运行,相互间关联较少。如果system调用成功,将返回0。
示例:
1 #include <stdio.h> 2 #include <stdlib.h> 3 int main() 4 { 5 system("ls -l"); //system(“clear”);表示清屏 6 return 0; 7 }
此外,system函数后面的参数还可以是一个可执行程序,例如:system(“/home/cp/1”);如果想要执行system后面进程的时候,不至于对当前进程进行阻塞,可以利用&将/home/cp/1调到后台运行。
2.2. fork函数
原型:
#include <unistd.h>
pid_t fork(void);
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。它和其他函数的区别在于:它执行一次返回两个值。其中父进程的返回值是子进程的进程号,而子进程的返回值为0.若出错则返回-1.因此可以通过返回值来判断是父进程还是子进程。
fork函数创建子进程的过程为:
使用fork函数得到的子进程是父进程的一个复制品,它从父进程继承了进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端,
而子进程所独有的只有它的进程号、资源使用和计时器等。
通过这种复制方式创建出子进程后,原有进程和子进程都从函数fork返回,各自继续往下运行,但是原进程的fork返回值与子进程的fork返回值不同,在原进程中,fork返回子进程的pid,而在子进程中,fork返回0,如果fork返回负值,表示创建子进程失败。(vfork函数)
示例:
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<unistd.h> 4 5 int main() 6 { 7 printf("parent process ID:%d ",getpid()); 8 pid_t iRet = fork(); 9 10 if(iRet < 0) 11 printf("Create child process fail! "); 12 13 else if(iRet == 0) 14 printf("child process id:%d ppid:%d ",getpid(),getppid()); 15 16 else 17 printf("parent process success, child id:%d ",iRet); 18 return 0; 19 }
//有人可能会有疑问:这里怎么if和else里面的语句都得到执行了,和我们以前的if…else结构相矛盾啊?此时相当于有两份main函数代码的拷贝,其中一份做的操作是if(iRet == 0)的情况;另外一份做的操作是else(父)的情况。所以可以输出2句话。提问:如何创建兄弟进程和爷孙进程?
2.3. exec函数族
exec*由一组函数组成
int execl(const char *path, const char *arg, ...)
exec函数族的工作过程与fork完全不同,fork是在复制一份原进程,而exec函数是用exec的第一个参数指定的程序覆盖现有进程空间(也就是说执行exec族函数之后,它后面的所有代码不在执行)。
path是包括执行文件名的全路径名
arg是可执行文件的命令行参数,多个用,分割注意最后一个参数必须为NULL。
例如,有个加法程序,从命令行接受两个数,输出其和 。
代码如下:
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<string.h> 4 5 int main(int argc, char *argv[]) 6 { 7 int a = atoi(argv[1]); 8 int b = atoi(argv[2]); 9 printf("%d + %d = %d",a,b,a+b); 10 return 0; 11 }
编译连接得到add.exe.
gcc –o add.exe add.c
然后在main.exe 中调用 add.exe 程序 ,计算3 和 4 的和。
main.c 的源程序为 :
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<string.h> 4 #include<unistd.h> 5 6 int main(int argc, char *argv[]) 7 { 8 execl("./add","./add","3","4",NULL); 9 return 0; 10 }
编译连接得,
gcc –o main.exe main.c
然后运行 。./main.exe。
在运行main.exe的过程中会通过execl启动之前的add.exe 程序。
2.4popen函数
popen函数类似于system函数,与system的不同之处在于它使用管道工作。原型为:
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
command为可执行文件的全路径和执行参数;
type可选参数为”r”或”w”,如果为”w”,则popen返回的文件流做为新进程的标准输入流,即stdin,如果为”r”,则popen返回的文件流做为新进程的标准输出流。
如果type是“r”,(即command命令执行的输出结果作为当前进程的输入结果)。被调用程序的输出就可以被调用程序使用,调用程序利用popen函数返回的FILE*文件流指针,就可以通过常用的stdio库函数(如fread)来读取被调用程序的输出;
如果tpye是“w”,(即当前进程的输出结果作为command命令的输入结果)。调用程序就可以用fwrite向被调用程序发送数据,而被调用程序可以在自己的标准输入上读取这些数据。
pclose等待新进程的结束,而不是杀新进程。
示例:
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<unistd.h> 4 #include<string.h> 5 int main() 6 { 7 FILE *read_fp; 8 char buf[1024]; 9 int chars_read; 10 11 read_fp = popen("ls","r"); 12 if(read_fp != NULL) 13 { 14 while(memset(buf,0,sizeof(buf)), 15 (chars_read = fread(buf,sizeof(char),1023,read_fp)) > 0) 16 { 17 buf[chars_read-1] = '