linux系统编程大纲
1. 进程概念、进程诞生与死亡、进程函数接口、进程意义。
2. 进程通信方式,有名管道、无名管道、信号、消息队列、共享内存、信号量。
3. 进程信号集、如何设置阻塞属性。
4. 线程概念,线程与进程区别?线程诞生与死亡,函数接口。
5. 线程同步互斥方式,有名信号量,无名信号量,互斥锁,读写锁,条件变量。
6. 拓展 -> 线程池 -> 同时处理多个任务。
目录
一、进程概念
1. 什么是程序?什么是进程?二者有什么区别?
程序是一堆待执行的代码。 -> 静态的文本数据。 project.c (C程序) / project (可执行程序)
进程是当程序被CPU加载时,根据每一行代码做出相应的动作,才能形成一个真正动态的过程,那么这个过程就称之为进程! -> 动态过程!
2. 如何在linux中开启新的进程?
在linux执行程序即可。
程序: project
开启新的进程: ./project -> 开启进程!
3. 当程序被执行时,除了在内存空间中分配空间之外,还会分配一个task_struct结构体给进程。
也就是说:每启动一个进程,就会得到一个task_struct结构体!
结构体在哪里? -> 其实在一个头文件中: /usr/src/linux-headers-3.5.0-23/include/linux/sched.h ->1229行。
4. 进程是资源分配的最小单位。所有的进程都是1号进程的子进程,一个进程至少有一个线程,也可以有多个线程。
5. 进程内存分布:每个进程一般都拥有4G的虚拟内存(真实并没有)。创建一个子进程,子进程也拥有独立的4G虚拟内存,并且复制父进程的运行状态(代码都是一样的,并且代码运行到哪都一样)
二、关于查看进程信息的linux命令
1. 查看整个系统所有进程的关系网 -> pstree
gec@ubuntu:~$ pstree
init─┬─NetworkManager───{NetworkManager}
├─accounts-daemon───{accounts-daemon}
├─acpid
├─anacron
├─gnome-terminal─┬─bash───pstree
init进程称之为"祖先进程"
init进程有5个子进程,分别是:NetworkManager、 accounts-daemon、acpid、anacron、gnome-terminal
gnome-terminal进程有1个子进程是bash进程
bash进程有1个子进程是pstree进程。
2. 查看系统所有进程的PID号 -> ps -ef
gec@ubuntu:~$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 23:04 ? 00:00:00 /sbin/init -> 祖先进程
gec 2250 1 0 23:04 ? 00:00:01 gnome-terminal
gec 2259 2250 0 23:04 pts/0 00:00:00 bash
gec 2646 2259 0 23:57 pts/0 00:00:00 ps -ef
UID -> 用户名,谁创建这个进程的!
PID -> 自身进程的ID号
PPID -> 父进程的PID号
CMD -> 进程的名字
祖先进程的PID一定是等于1
3. 查看进程CPU占用率/当前系统总进程数/进程状态个数 -> top
gec@ubuntu:~$ top -> 按"q"返回终端!
Tasks: 151 total -> 当前系统总进程数
3 running -> 3个进程正在运行
148 sleeping -> 148个正在睡眠
0 stopped -> 0个暂停
0 zombie -> 0个僵尸
%CPU
997 root 20 0 99824 29m 6700 S 1.7 3.0 0:09.39 Xorg
2250 gec 20 0 92272 15m 11m S 0.7 1.6 0:02.33 gnome-terminal
%CPU -> CPU使用率 -> 动态更新
三、进程诞生与死亡
1、进程的状态
当执行一个程序,就诞生了一个新的进程。
就绪态 ---》等待cpu资源,不占用CPU资源,不运行代码
运行态 ---》得到cpu资源,运行代码
僵尸态 ---》占用cpu的资源,不运行代码,不可以切换到就绪态/运行态
死亡态 ---》正常的结束,不占用cpu资源,不运行代码
睡眠态 ---》sleep()函数之类的
暂停态 ---》收到19) SIGSTOP信号(查看信号的命令:kill -l,占用CPU资源),不运行代码,可以切换到就绪态/运行态
进程的生老病死
2. 什么是占用CPU资源?
其实代表task_struct结构体资源没有被释放。
3. 进程的生老病死过程需要注意点:
1)进程在暂停态收到继续信号时,切换到就绪态,而不是运行态。
2)进程退出时,一定会变成僵尸态。
3)进程不可以同时拥有两个父亲。
4)孤儿进程特点: 当自己还在运行态时,父进程已经退出了,马上寻找init作为自己的继父。
5)init进程特点:一定会帮所有的孤儿回收资源。
四、信号
查看命令:kill -l
给进程发送信号:1、kill + 进程号
2、kill + -代表信号的数字 + 进程名
进程可以设定要不要阻塞某个信号
进程收到信号,有三种反应:1、默认信号本来的动作
2、忽略,但是9号信号和19号信号不能被忽略
3、做自己自定义的事
五、进程创建
1、创建新的进程
#include <unistd.h>
函数:pid_t fork(void);
说明:子进程跟父进程运行在分开的内存空间,意味着父子进程是互相独立的,谁先运行不一定。
返回值: -1 : 创建子进程失败
0 : 表示在子进程中,但是0并不是子进程的id,只是为了区分父子进程
>0 : 表示在父进程中,返回的pid是子进程的id
创建的子进程会复制一份一模一样的父进程的资源给子进程,包括程序计数器的指针,导致了子进程不能从头开始执行父进程的代码,只能执行fork()后面的代码
程序计数器:用来存储程序执行的指令,处理器会执行程序计数器中的指令
2、查看自身的PID号以及父进程的PID号
getpid() getppid() -> man 2 getpid
功能: 获取ID号
使用格式:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); -> 获取自身ID
pid_t getppid(void); -> 获取父进程ID
参数: 无
返回值: getpid() 返回值自身的ID号
getppid() 返回父进程的ID号
六、进程回收
理论上,子进程应该由父进程回收,但是有时候子进程没结束父进程就结束了,那么子进程理论上应该由1号进程来收养。
怎么避免子进程未结束父进程就结束了?
等待回收子进程:
#include <sys/types.h>
#include <sys/wait.h>
1、pid_t wait(int *status);
功能: 使得子进程改变状态 wait for process to change state -> 子进程从僵尸态变成死亡态
status:填int*指针,那么指针保存子进程的退出状态
填NULL,不关注子进程的退出状态,只回收资源
返回值:
成功: 退出的子进程的ID
失败: -1
2、waitpid(pid_t pid, int *wstatus, int options);
参数:pid:
小于-1,取pid的绝对值,等待这个绝对值id进程组中的任意子进程
等于-1,等待任意进程,相当于wait();函数的效果
等于0,等待当前进程组的任意子进程
大于0,等当id位pid的子进程
七、进程退出
1. 进程退出函数作用是什么?
当进程执行了exit()/_exit(),整个进程就会马上退出。
2. 函数如何使用?
exit() -> man 3 exit
#include <stdlib.h>
void exit(int status);
_exit() -> man 2 _exit
#include <unistd.h>
void _exit(int status);
status:进程的退出状态 -> 返回父进程(前提父进程必须主动回收资源)
0 -> 代表进程正常退出
非0 -> 代表进程异常退出
返回值:无
3. exit()/_exit()有什么区别?
这两个函数功能类似,都是用于退出进程,但是两者缓冲区处理机制不一样。
exit() -> 进程退出时,输出缓冲区数据,再退出!
_exit() -> 进程退出时,不会输出缓冲区数据,直接退出!
4.在程序中,exit()函数与return语句区别?
拓展结论:由于在main函数内调用return相当于程序的退出,所以在main函数内执行exit()与return是一样的。
八、从内存角度分析父子进程资源问题
父进程fork出一个子进程之后,会将自己资源拷贝一份资源(除了PID号)到子进程中。
练习1:让父进程打印hello,子进程打印apple,要求子进程一定要先打印。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t x;
x = fork();
if(x > 0) //父进程
{
usleep(100000);
printf("helloworld!\n");
}
if(x == 0)//子进程
{
printf("apple!\n");
}
return 0;
}
结论:使得父子进程处理不同的任务,只需要判断返回值即可!
练习2: 在父进程中打印自己与孩子的PID号,在子进程中打印自己与父亲的PID号。要求子进程先执行。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t x;
x = fork();
if(x > 0) //父进程
{
usleep(100000);
printf("parent pid = %d\n",getpid());
printf("child pid = %d\n",x);
}
if(x == 0)//子进程
{
printf("apple!\n");
printf("child pid = %d\n",getpid());
printf("parent pid = %d\n",getppid());
}
return 0;
}
练习3: 验证孤儿进程的父进程是祖先进程。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t x;
x = fork();
if(x > 0) //父进程
{
sleep(2); //2秒之后,子进程就是一个孤儿进程
}
if(x == 0)//子进程
{
printf("my parent id = %d\n",getppid()); //id = 2892
sleep(5);
printf("my parent id = %d\n",getppid()); //id = 1
printf("helloworld!\n");
}
return 0;
}
练习4:父进程还在,并且主动回收资源。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t x;
x = fork();
if(x > 0) //父进程
{
wait(NULL); //阻塞等待子进程的退出,然后再帮子进程回收资源。
printf("hello!\n");
}
if(x == 0)//子进程
{
printf("apple!\n"); -> 退出时有父进程帮自己回收资源,所以我就不找继父。
}
return 0;
}
练习5:验证进程的生老病死.jpg 情况二,看看是不是父进程不退出,那么子进程永远都是一个僵尸
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t x;
int i;
x = fork();
if(x > 0)
{
for(i=0;i<20;i++)
{
printf("%d\n",i);
sleep(1);
}
}
if(x == 0)
{
sleep(3); -> 在3~20秒内,子进程都是僵尸
printf("I am exit!\n");
}
return 0;
}