进程标识
每一个linux都有唯一一个进程标识,且为非负数。进程的其他参数可以通过相应的函数获得,函数声明在头文件“unistd.h”中。
函数声明 | 功能 |
---|---|
pid_t getpid(id) | 获得进程ID |
pid_t getppid(id) | 获得父进程ID |
pid_t getuid(id) | 获得进程实际用户ID |
pid_t getcuid(id) | 获得进程有效用户ID |
pid_t getgid(id) | 获得进程实际组ID |
pid_t getegid(id) | 获得进程有效组ID |
实际用户:标识运行进程的用户
有效用户:程序以怎么的身份运行程序
实际组:实际用户所属的组
有效组:有效用户所属的组
linux进程的结构
代码段存放程序可执行代码。
数据段存放程序全局变量、常量、静态变量
堆:存放动态分配的内存变量
栈:存放函数调用
linux进程状态
- 运行状态
- 可中断等待状态
- 不可中断等待状态
- 僵死状态
- 停止状态
用ps命令可以查看进程的状态。运行状态R,可中断等待状态S,不可中断等待状态D,僵死状态Z,停止状态为T。
进程的控制
用来对进程进行控制的主要系统调用如下
fork:用于创建一个新进程
exit:用于终止进程
exec:用于执行一个应用程序
wait:将父进程挂起,等待子进程终止
getpid:获取当前进程的进程ID
nice:改变进程的优先级
进程的PID
PID_MAX=0x8000(可改),因此进程号的最大值为0x7fff,即32767。其中0-299保留给daemon进程。可以通过/proc/sys/kernel/pid_max修改。
内核的PID号是会一直往上增长的,一般不会使用旧的PID号。但如果PID号达到最大值,内核就会重新开始找小的,没有使用的PID号。
查看进程的详细信息
查看进程详细信息的常用三种方法有ps命令,top命令和cat /proc/pid/status。其中ps和top的显示比较直观,下面把cat /proc/pid/status的方法详细说下,这里是内核创建进程后存放进程信息最原始的地方。
1.先用ps命令找到你关心的进程的pid号,如我想看bash进程的pid号
ps -aux |grep bash
然后
cat /proc/pid/status 这里的pid要用你 查到的pid号代替。如cat /proc/8311/status
输出如下
svauto@ubuntua:/proc/8311$ cat /proc/8311/status
Name: bash
State: S (sleeping)
Tgid: 8311
Ngid: 0
Pid: 8311
PPid: 2632
TracerPid: 0
Uid: 1001 1001 1001 1001
Gid: 1001 1001 1001 1001
FDSize: 256
Groups: 27 1001
NStgid: 8311
NSpid: 8311
NSpgid: 8311
NSsid: 8311
VmPeak: 24908 kB
VmSize: 24844 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 6004 kB
VmRSS: 5972 kB
VmData: 2068 kB
VmStk: 136 kB
VmExe: 976 kB
VmLib: 2308 kB
VmPTE: 68 kB
VmPMD: 12 kB
VmSwap: 0 kB
HugetlbPages: 0 kB
Threads: 1
SigQ: 0/15584
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000380004
SigCgt: 000000004b817efb
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
Seccomp: 0
Cpus_allowed: ffffffff,ffffffff,ffffffff,ffffffff
Cpus_allowed_list: 0-127
Mems_allowed: 00000000,00000001
Mems_allowed_list: 0
voluntary_ctxt_switches: 2897
nonvoluntary_ctxt_switches: 520
各字段解释
参数 | 解释 |
---|---|
Name | 应用程序或命令的名字 |
State | 任务的状态,运行/睡眠/僵死/ |
SleepAVG | 任务的平均等待时间(以nanosecond为单位),交互式任务因为休眠次数多、时间长,它们的 sleep_avg 也会相应地更大一些,所以计算出来的优先级也会相应高一些。 |
Tgid | 线程组号 |
Pid | 任务ID |
Ppid | 父进程ID |
TracerPid | 接收跟踪该进程信息的进程的ID号 |
Uid | Uid euid suid fsuid |
Gid | Gid egid sgid fsgid |
FDSize | 文件描述符的最大个数,file->fds |
Groups | |
VmSize(KB) | 任务虚拟地址空间的大小 (total_vm-reserved_vm),其中total_vm为进程的地址空间的大小,reserved_vm:进程在预留或特殊的内存间的物理页 |
VmLck(KB) | 任务已经锁住的物理内存的大小。锁住的物理内存不能交换到硬盘 (locked_vm) |
VmRSS(KB) | 应用程序正在使用的物理内存的大小,就是用ps命令的参数rss的值 (rss) |
VmData(KB) | 程序数据段的大小(所占虚拟内存的大小),存放初始化了的数据; (total_vm-shared_vm-stack_vm) |
VmStk(KB) | 任务在用户态的栈的大小 (stack_vm) |
VmExe(KB) | 程序所拥有的可执行虚拟内存的大小,代码段,不包括任务使用的库 (end_code-start_code) |
VmLib(KB) | 被映像到任务的虚拟内存空间的库的大小 (exec_lib) |
VmPTE | 该进程的所有页表的大小,单位:kb |
Threads | 共享使用该信号描述符的任务的个数,在POSIX多线程序应用程序中,线程组中的所有线程使用同一个信号描述符。 |
SigQ | 待处理信号的个数 |
SigPnd | 屏蔽位,存储了该线程的待处理信号 |
ShdPnd | 屏蔽位,存储了该线程组的待处理信号 |
SigBlk | 存放被阻塞的信号 |
SigIgn | 存放被忽略的信号 |
SigCgt | 存放被俘获到的信号 |
CapInh Inheritable | 能被当前进程执行的程序的继承的能力 |
CapPrm Permitted | 进程能够使用的能力,可以包含CapEff中没有的能力,这些能力是被进程自己临时放弃的,CapEff是CapPrm的一个子集,进程放弃没有必要的能力有利于提高安全性 |
CapEff Effective | 进程的有效能力 |
创建进程
创建进程可以由“操作系统”和“父进程”创建。操作系统创建的进程没有继承关系。父进程创建的子进程,可以继承父进程的几乎所有资源。
一、fork函数创建进程
需要包含头文件
#include <unistd.h>
fork函数的示例代码如下
#include "stdio.h"
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
printf("Process Creating study!\n");
pid = fork();
printf("point 1");
switch(pid)
{
case 0:
printf("Child process is running ,CurPid is %d, ParentId is %d\n",pid, getppid());
break;
case -1:
printf("Process creating faild\n");
break;
default:
printf("Parent process is running ,CurPid is %d, ParentId is %d\n",pid, getpid());
}
printf("exit\n");
}
很多书会说fork在使用的时候,会返回两次。但是我觉得这样的描述是不妥的。其实fork并没有返回两次,看到有两次是因为fork会创建新的一个进程,且子进程的代码和父进程的代码是一样的。并且进程的运行位置都是一样的,有区别的是,子进程得到了0的返回值。父进程得到了子进程的PID号。
程序运行结果:
fork函数创建进程的特点
1.子进程和父进程是两个独立的程序。他们互不干扰的运行。
2.子进程有自己唯一的PID
3.父进程退出后,子进程继承给祖先进程,一级一级的上升,直到继承给init进程。
4.子进程共享父进程打开的文件描述符,但是父进程对文件描述符的修改,不会影响子进程
5.子进程不继承父进程设置的警告
6.子进程的未决信号集被清空。
二、vfork函数创建进程
vfork的本质也是最终调用fork来进程,但是还是有细微的差别,如下
1.使用fork创建的父子两个进程是完全独立的。但是vfork创建的子进程共享父进程的地址空间。因此子进程改了数据,父进程是可以看见的。
2.fork创建的父子进程执行先后顺序由内核进程调度机制决定。vfork创建的子进程需要调用exec或exit之后,父进程才能继续执行。
fork和vfork创建进程的方法是一样的。如下代码所示
#include "stdio.h"
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int globVar =5;
int main(void)
{
pid_t pid;
int var =1,i;
printf("fork is diffirent with vfork!\n");
printf("now globVar =%d,var=%d\r\n",globVar,var);
pid = vfork();
//pid = fork(); //解开注释就能使用任意一个
switch(pid)
{
case 0:
i=2;
while(i-->0)
{
printf("Child process is running \n");
globVar++;
var++;
sleep(1);
}
printf("child globvar=%d,var=%d\r\n",globVar,var);
break;
case -1:
printf("Process creating faild\n");
break;
default:
i=2;
while(i-->0)
{
printf("Parent process is running \n");
globVar++;
var++;
sleep(2);
}
printf("parent gloVar =%d,var=%d\n",globVar,var);
break;
}
exit(0);
}
这个程序,把下面代码任意选择一个编译并运行。就可以看到vfork的地址空间父子进程是共享的。
pid = vfork();
//pid = fork(); //解开注释就能使用任意一个
如下vfork的执行结果,刚开始全局变量globVar=5,局部变量var=1.子进程对局部变量和全局变量都做了修改globVar=7,var=3.,然后父进程继续做两次+1操作。最终globVar=9,var=5.说明子进程的操作对父进程有影响。
以下是fork的结果,可以看出子进程和父进程的结果互不影响,各算各的,执行顺序也是无法预计。
三、创建守护进程
守护进程就是在后台运行的进程。用来做服务进程非常有用。创建步骤比较多。但都是套路,直接写上去就行了。
守护进程一般在系统系统的时候自动启动。因此都是修改启动脚本(如/etc/rc.d)来完成启动。
创建守护进程需要有如下特性:
- 进程后台运行,fork一个子进程,然后父进程退出。
- 将进程变成会话组长,调用setsid。
- 禁止进程重新打开控制终端。可以用fork创建一个子进程,然后父进程退出
- 关闭不再需要的文件描述符。避免浪费资源或造成文件系统无法卸载。
- 更改当前目录为根目录。
- 设置屏蔽字为0,使用umask(0)。
- 处理SIGCHLD信号。这一步不是必须的。避免出现僵尸进程。
代码如下
#include "stdio.h"
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <time.h>
#include <syslog.h>
#include <stdlib.h>
int init_daemon(void)
{
int pid;
int i;
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
signal(SIGHUP,SIG_IGN);
pid = fork();
if(pid>0)
{
exit(0);
}
else if(pid <0)
{
return -1;
}
setsid();
pid = fork();
if(pid >0)
{
exit(0);
}
else if(pid <0)
{
return -1;
}
for(i=0;i<NOFILE;close(i++));
chdir("/");
umask(0);
return 0;
}
int main()
{
time_t now;
init_daemon();
syslog(LOG_USER|LOG_INFO,"守护程序\n");
while(1)
{
sleep(8);
time(&now);
syslog(LOG_USER|LOG_INFO,"系统时间:\t%s\t\n",ctime(&now));
}
}
编译运行
gcc -o daemon daemon.c
./daemon
ps -aux |grep daemon
或者ps -ef
查看进程,有如下输出
svauto 103299 0.0 0.0 4352 80 ? S 14:56 0:00 ./daemon
查看系统日志。在/var/log目录下的sudo tail syslog。结果如下图。
四、程序退出
1.正常退出。可用方法
- 在main中return
- 调用exit
- 调用_exit
2.异常退出。可用方法
- 调用about函数
- 进程收到某个信号,而该信号终止进程
五、执行新进程
执行新程序的函数
这几个函数的区别与环境变量和传参方法有关。
函数 | 传参 |
---|---|
execve | 通过路劲方式调用,argv,envp对应新程序的argv ,envp |
execv | 通过路劲方式调用,argv对应新程序的argv |
execl | 和execv类似,不过argv参数数量不确定 |
execle | 和execl类似,但是要指定环境变量 |
execvp | 和execv类似,但可以在环境变量中找可执行程序 |
execlp | 类似execle , 但可以在环境变量中找可执行程序 |
exec函数族除了execve是系统调用,其他都是库函数。
exec函数返回错误表
实例代码
程序1,用来被调用
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc,char *argv[],char **environ)
{
int i;
printf("i am a process image!\n");
printf("My pid = %d, parentpid = %d\n",getpid(),getppid());
printf("uid = %d,gid = %d\n",getuid(),getgid());
for(i= 0 ;i<argc;i++)
{
printf("argv[%d]:%s\n",i,argv[i]);
}
}
程序2:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc,char *argv[],char ** environ)
{
pid_t pid;
int stat_val;
printf("Exec example !\n");
pid = fork();
switch(pid)
{
case -1:
perror("process creation failed\n");
exit(1);
case 0:
printf("Child process is running\n");
printf("My pid =%d,parentpid=%d\n",getpid(),getppid());
execve("processimage",argv,environ);
printf("process never go to here!\n");
exit(0);
default:
printf("Parent process is running !\n");
break;
}
wait(&stat_val);
exit(0);
}
运行程序
做个测试,如果把execveDemo删除,运行程序就会报错,如下
六、等待进程结束wait 和waitpid
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t pid;
char *msg;
int k;
int exit_code;
printf("Study how to get exit code\n");
pid =fork();
switch(pid)
{
case 0:
msg = "Child process is running";
k = 5;
exit_code = 37;
break;
case 1:
perror("process create faild\n");
exit(1);
default:
exit_code= 0;
break;
}
//父进程会执行这段代码,
if(pid!=0)
{
int stat_val;
pid_t child_pid;
child_pid = wait(&stat_val);
printf("Child process has exited,pid = %d\n",child_pid);
if(WIFEXITED(stat_val))
printf("child exited with code %d\n",WEXITSTATUS(stat_val));
else
printf("child exited abnormallly\n");
}
else
{//子进程暂停5秒,在这个时候可以运行ps -aux查看父进程状态
while(k-->0)
{
puts(msg);
sleep(1);
}
}
exit(exit_code);
}
运行程序输出
可以看出父进程在执行wait后,就处于等待状态,知道子进程退出后,父进程再继续执行。
如果子进程比父进程提前结束,子进程就会成为僵尸进程,占用着资源,无法释放。所以在子进程结束之前,需要让父进程调用wait。僵尸进程只有父进程结束了,才能释放资源。
八、常见的进程的其他操作
函数 | 功能 |
---|---|
getpid | 获取进程ID |
setuid | 设置实际用户id |
setgid | 设置有效用户ID |
nice | 改变进程优先级 |
getpriority | 获取优先级 |
setpriority | 设置优先级 |
遇到的错误总结:
如下图错误,程序语法是没有问题的,但是在最后链接的时候,因为找不到函数,就会出现链接错误,如下图,这时候需要增加头文件。