创建进程
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
pid_t id;
id = fork();
int i = 0;
if(id < 0)
{
perror("fork");
exit(1);
}
else if(id == 0)
{
for(i = 0; i < 10; i++)
{
printf("i am child,my pid = %d my parent pid = %d\n",getpid(),getppid());
sleep(1);
}
}
else
{
for(i = 0; i < 10; i++)
{
printf("i am parent, my pid = %d\n",getpid());
sleep(1);
}
}
}
子进程的pid为3932 父进程的pid为3931
孤儿进程
父进程中止后,子进程任然进行,此时子进程将被init进程收养。
进程的资源
fork函数时调用一次,返回两次。在父进程和子进程中各调用一次。子进程中返回值为0,父进程中返回值为子进程的PID。程序员可以根据返回值的不同让父进程和子进程执行不同的代码。子进程是父进程的副本,获得了父进程数据空间、堆和栈的副本;父子进程并不共享这些存储空间,共享正文段(即代码段);因此子进程对变量的所做的改变并不会影响父进程。一般来说,fork之后父、子进程执行顺序是不确定的,这取决于内核调度算法。进程之间实现同步需要进行进程通信。
#include <time.h>
#include <pwd.h>
#include <grp.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
int g_iNum=10; //全局变量 静态存储区
int main()
{
char *p = (char *)malloc(20); //堆
strcpy(p,"world");
int stack_num = 100; //栈 局部变量
pid_t pid;
pid=fork();
if(0==pid)
{//这里是子进程的代码
printf("----------------------------------------------------------------\n");
printf("I am child process,g_iNum=%d\n",g_iNum);
printf("I am child process,%s\n",p);
printf("I am child process,stack_num =%d\n",stack_num);
strcpy(p,"hello");
g_iNum=5;
stack_num = 200;
printf("----------------------------------------------------------------\n");
printf("I am child process,g_iNum=%d,g_iNum_addr=%p\n",g_iNum,&g_iNum);
printf("I am child process,%s,p_addr = %p\n",p,p);
printf("I am child process,stack_num = %d,stack_num_addr = %p\n",stack_num,&stack_num);
}else{
//这里是父进程的代码
printf("----------------------------------------------------------------\n");
printf("I am parent process,g_iNum=%d\n,g_iNum_addr",g_iNum,&g_iNum);
printf("I am parent process,%s,p_addr = %p\n",p,p);
printf("I am parent process,stack_num = %d\n",stack_num);
sleep(1);
printf("----------------------------------------------------------------\n");
printf("I am parent process,g_iNum=%d,g_iNum_addr=%p\n",g_iNum,&g_iNum);
printf("I am parent process,%s,%p\n",p,p);
printf("I am parent process stack_num = %d,stak_num_addr = %p\n",stack_num,&stack_num);
return 0;
}
}
可以看到,其值发生了改变,这也说明,父子进程的堆区数据、栈区数据、全局数据是不共享的。
但是所指向的虚拟地址是一样的,也就是说不同的进程的变量却有相同的虚拟地址(注:程序中变量的地址都是虚拟地址,而非物理地址)。原因是内核会为每个进程分配4G的虚拟地址空间,这4G的虚拟地址空间地址分布都是一样的,由于子进程虚拟地址空间的数据都是从父进程中拷贝而来的,都是一样的,因此相同的数据在4G的虚拟地址空间中的分布也是一样的。那为什么相同的虚拟地址(变量地址)最终会得到不同的变量值呢?原因是虽然虚拟地址分布是一样的,但是由于相同的虚拟地址映射到不同的物理地址,所以我们才会得到不同的变量值。
数据传输
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char* argv[])
{
pid_t pid;
int fd;
char *info = "hello world";
char buf[64] = {0};
int nBytes;
fd = open("file",O_RDWR|O_CREAT,0644);
if(fd < 0)
{
perror("open file failed");
exit(1);
}
printf("before fork\n");
if((pid = fork()) < 0)
{
perror("fork error");
}
else if(pid == 0)
{
if((nBytes = write(fd,info,strlen(info)))<0)
{
perror("write failed");
}
exit(0);
}
else
{
sleep(1);
lseek(fd,0,SEEK_SET);
if((nBytes = read(fd,buf,64)) < 0)
{
perror("read failed");
}
printf("%s\n",buf);
}
exit(0);
}
父进程首先打开了一个文件file。当调用fork创建紫进程后,子进程继承父进程中打开的文件,也就是说父、子进程共享该文件。
linux会为进程打开三个文件,标准输入文件、标准输出文件、标准错误文件。
子进程会继承父进程打开的文件
两种典型的用法
一个父进程希望复制自己,使父,子进程同时执行不同的代码段。这在网络服务进程中常见--父进程等待委托者的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求,父进程则继续等待下一个服务请求。
一个进程要执行不同的程序。 这对Shell是常见的情况。在这种情况下,子进程在从fork返回后立即调用execve来执行新程序。
后记:
【1】进程的基础
1)进程与程序
程序:保存在磁盘上的一组指令的有序集合。静态的,没有任何的执行的概念。
进程:是程序动态运行的一次执行过程。动态的,具有一定的生命周期,包括创建,调度,消亡。
2)进程由2部分组成
用户空间资源:数据段,代码段,堆栈段,BSS段,进程资源,资源自动释放
内核空间资源:进程标识符,PCB(进程控制块),进程的属性信息,不会主动的释放资源,要需要人为主动回收。
3)主要的进程标识
PID 进程PID
PPID 父进程PID
注意:PID是一个唯一的,正整数。
4)进程的执行过程
创建:每启动(运行)一个进程,内核就会为当前的进程分配内存空间,保存变量,代码等。
调度:CPU的(优先级)调度,上下文的切换(用户空间和内核空间的切换)。
消亡:进程结束,需要回收资源,主动回收(调用相关的函数接口)。
5)Linux中的进程包含三个段
数据段
代码段
堆栈段
PCB(进程控制块)
6)进程的种类:
交互进程:
该类进程是由shell控制和运行的。交互进程既可以在前台运行,也可以在后台运行,可以与用户直接交互,
且受终端的控制。
前台进程-----------受终端的控制
后台进程------------不受终端控制
注意:将前台进程变成后台进程 ./a.out &
批处理进程:
该是一个进程序列,负责按照顺序启动,该类进程不属于某个终端,不与用户进行交互,在后台运行,称之为进程的集合。
守护进程:
长期运行在后台做某些服务,一般在Linux启动时开始运行,系统结束时停止运行。
本身属于后台进程, 不受终端控制。自己做为会话组组长。【2】进程的相关命令
top 动态查看进程的属性信息(PR,NI优先级)
ps
px ajx 查看进程的属性信息
ps aux 查看进程的属性信息
特殊:0号进程:内核进程
1号进程:init进程
每一个进程都有父进程,除了0号进程(理解用2叉树理解)
ps axj 指令:
ps aux | grep 可执行程序的名称
ps aux/-aux | grep a.out
PPID PID PGID SID TTY TPGID STAT (***) UID TIME COMMAND
0 1 1 1 ? -1 Ss 0 0:01 /sbin/init
父亲进程id 进程号 进程组id 会话id 终端 终端进程组id 状态 用户id 时间 进程命程
int getdtablesize(void); // 得到最大能打开文件描述符个数
Here are the different values that the s, stat and state output specifiers (header "STAT" or "S") will
display to describe the state of a process:
D uninterruptible sleep (usually IO) // 不可中断睡眠态
R running or runnable (on run queue) // 运行态(正在运行,等待运行)
S interruptible sleep (waiting for an event to complete) // 睡眠态
T stopped, either by a job control signal or because it is being traced. // 停止态
X dead (should never be seen) // 死亡态,瞬间发生,程序员不可见
Z defunct ("zombie") process, terminated but not reaped by its parent. // 僵尸态For BSD formats and when the stat keyword is used, additional characters may be displayed:
< high-priority (not nice to other users) // 高优先级进程
N low-priority (nice to other users) // 低优先级进程
L has pages locked into memory (for real-time and custom IO)
s is a session leader // 会话组组长
注意:进程---》进程组组长---》会话组组长
l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do) // 进程中包含了线程
+ is in the foreground process group. // 前台进程
kill
1)kill -l 查看信号
SIGINT //默认属性结束进程 ctrl+c
SIGQUIT //默认属性结束进程 ctrl+ \
SIGKILL // 杀死进程
SIGUSR1 // 用户自定义的信号,默认结束进程
SIGUSR2 // 用户自定义的信号,默认结束进程
SIGCHLD // 儿子进程死亡,系统会给父亲进程发送 SIGCHLD 信号
SIGALRM // 闹钟信号,默认结束进程
SIGSTOP // 停止信号, 进程进入暂停态 ctrl + z
SIGTSTP // 停止信号
SIGCONT // 恢复运行信号
注意:SIGKILL,SIGSTOP不能被捕捉,忽略,屏蔽。
2)kill 向指定的进程发送信号
kill +信号的名称(通过kill -l查看得到的信号)+PID 向执行的进程发送信号。
注意:只有 kill -9 + pid 是具有杀死进程的功能,其他的数字没有。
bg 将挂起的进程放到后台执行(暂停进程)
./aout & 将普通执行的程序放到后台执行
fg 将后台进程放到前台执行
注意:bg 1
bg 2
fg 1
fg 2
...
nice 在进程启动前改变进程的优先级,并将进程运行起来
sudo nice -n (优先级)(可正可负)+ ./a.out
sudo nice -n -10 ./3
renice 在进程运行过程中改变进程的优先级
sudo renice -n +(优先级数)+pid
sudo renice -n -15 pid
ps -le
【3】进程的相关系统调用
源进程与新创建进程的关系:子进程精确复制父进程的代码,复制了代码段,数据段,堆栈段,BSS段,文件
描述符,缓冲区空间;空间各自独立,PID,PPID,ino号,进程PCB控制块没有复制。
1)进程的创建---fork
#include <unistd.h>
pid_t fork(void);
函数的功能:创建新的子进程(在已有的进程的基础上)
参数:无
返回值:pid < 0 出错 没有创建子进程
pid =0 执行子进程
pid >0 执行父进程
注意:孩子进程从fork之后执行,因为在fork之后才出现父子进程。
父子进程的执行顺序不确定,谁先抢占到CPU谁先执行。
谁先执行并不代表谁先执行完。
父子进程没有分开执行
父子进程分开执行
2)getpid
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
函数的功能:获取当前进程的PID
参数:无
返回值:当前进程的PID
getppid
pid_t getppid(void);
函数的功能:获取当前进程的父进程PID
参数:无
返回值:当前进程的父进程PID
3)子进程对父进程缓冲区的处理
结论:创建父子进程,子进程精确复制父进程,复制包括数据段,代码段,BSS段,堆空间,栈空间,文件描述符(特例), 用户空间的缓冲区,当子进程复制父进程的缓冲后,随着子进程结束,会刷新缓冲区当中的数据,所以造成在fork之前的打印语句会执行2次。
注意:子进程是在fork语句之后执行的。
子进程对父进程打开的文件描述符的处理
注意:虽然子进程复制用户空间父进程的文件描述符,但是在内核空间对应的文件管理表项中的文件指针是共享的,====》共享偏移量
4)僵尸进程和孤儿进程
1>僵尸进程是如何产生?
父进程活着,儿子进程死了,但是父进程并没有为儿子进程回收尸体,从而产生僵尸进程
2>如何避免僵尸进程?
1)形成孤儿进程---解决僵尸进程。
2)父进程活着,儿子进程死了,让父亲进程主动调用回收函数去回收儿子进程的资源(wait,waitpid)
3>孤儿进程是如何产生的?
当父亲进程优先于子进程结束,子进程被init(1号进程)收养,此时将这个进程称之为孤儿进程。
注意:孤儿进程的产生时一个正常的现象。
5)进程的退出
exit----结束一个进程,会刷新缓冲区
_exit-----结束一个进程,不会刷新缓冲区
注意:如果程序正常结束也会刷新缓冲区exit/_exit与return的区别
1)return 结束一个函数体,函数体结束,进程不一定结束
2)exit/_exit结束一个进程,进程结束,函数体一定结束。
6)wait/waitpid
wait----回收任意一个子进程的资源
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
函数的功能:阻塞等待回收子进程的资源
参数:status 进程结束时的状态信息
返回值:成功返回回收子进程的pid
失败-1
waitpid---自己指定回收某一个子进程的资源。
pid_t waitpid(pid_t pid, int *status, int options);
函数的功能:回收特定子进程的资源
参数:pid 要回收子进的pid
pid <-1 回收当前调用进程|pid|等于同组pid的任意子进程
pid =-1 回收任意一个子进程的资源 ==wait
pid =0 回收同组进程下的任意一个子进程
pid >0 回收指定子进程的pid
status 子进程结束时的状态
options 0 阻塞
WNOHANG 非阻塞
返回值:成功返回子进程pid
WNOHNAG 没有子进程结束 0
失败-1;
7)阻塞与非阻塞(银行办业务)
阻塞:一直等待条件的发生,直到得到结果,保证了结果。
非阻塞:等待条件的发生,但是不一定等待到结果,保证了时间。
8)exec族函数---在子进程当中启动新的程序
execl
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
函数的功能:在子进程中运行新的程序代码
参数:path 要执行程序的路径+名称
注意:path必须是一个绝对路径
execlp
int execlp(const char *file, const char *arg, ...);
函数的功能:在子进程中运行新的程序代码
参数:path 要执行程序的路径+名称
注意:文件的名称可以不用写绝对路径,可以在当前的环境变量下去寻找。
execle
int execle(const char *path, const char *arg,
..., char * const envp[]);
函数的功能:在子进程中运行新的程序代码
注意:path必须是绝对路径,最后要以一个新的指针数组结束。
execv
int execv(const char *path, char *const argv[]);
函数的功能:在子进程中运行新的程序代码
注意:path是一个绝对路径
只有2个参数,没有可变参数
结尾以指针数组结束
先定义一个指针数据,将要运行的命令写入指针数组,然后将指针数组的名称传入
system
#include <stdlib.h>
int system(const char *command);
函数的功能:在当前运行的进程当中启动新的程序
参数:command 命令
返回值:成功返回非负数
失败-1
9)守护进程
守护进程的定义:守护进程,也就是通常所说的Daemon进程,是Linux中的后台服务进程。它是一个生存期较长的进程,
通常独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件
守护进程常常在系统启动时开始运行,在系统关闭时终止
Linux系统有很多守护进程,大多数服务都是用守护进程实现的
守护进程创建的步骤:
1,创建子进程,父进程退出
fork
2,在子进程中创建新会话 ,并且成为会话组组长
setsid
3,改变当前目录为根目录
chdir
4,重设文件权限掩码
umask---放开权限
5,关闭文件描述符
getdtablesize
6,做服务
write
read