文章目录
1 进程基础
1.1 进程概念
进程是一个或多个线程的集合,集合会占用地址空间和系统资源。
一个程序文件,只是一堆执行的代码和部分待处理的数据,他们只有被加载到内存中,然后让CPU追条执行其代码,根据代码做出相应的动作,才形成一个真正“活的”、动态的进程,因此进程是一个动态变化的过程,是一出有始有终的戏,而程序文件只是一系列动作的原始蓝本,是一个静态的剧本。
1.2 命令
1.gec@ubuntu:/mnt/hgfs/…/code0517$ pstree
systemd─┬─ManagementAgent───6*[{ManagementAgent}]
├─ModemManager─┬─{gdbus}
│ └─{gmain}
├─NetworkManager─┬─{gdbus}
│ └─{gmain}
├─VGAuthService
├─accounts-daemon─┬─{gdbus}
│ └─{gmain}
├─acpid
2.gec@ubuntu:/mnt/hgfs/…/code0517$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 5月14 ? 00:01:00 /sbin/init auto noprompt
root 2 0 0 5月14 ? 00:00:00 [kthreadd]
root 4 2 0 5月14 ? 00:00:00 [kworker/0:0H]
root 6 2 0 5月14 ? 00:02:10 [ksoftirqd/0]
root 7 2 0 5月14 ? 00:00:34 [rcu_sched]
root 8 2 0 5月14 ? 00:00:00 [rcu_bh]
1.3 进程的状态
孤儿态:子进程还没有结束,父母进程先退出没有对其进行资源回收,则子进程会被送回“孤儿院”(系统进程),系统进程会对其进行资源回收。
僵尸态:子进程退出,父母进程依然“健在”,但是也没有对其进行资源回收,(由于父母进程还在,所以系统不会把其看作是孤儿)这样子进程就会“僵死”在那,变成僵尸进程。我们平时编程要避免产生僵尸进程,因为过多的僵尸进程会占用浪费大量的系统资源。
1.4 重要API
[1]创建进程
pid_t fork(void);
返回值:
成功:非负数 0:child 正数:parent
失败:-1
备注:
1.子进程会从fork()返回值的下一条逻辑语句开始运行。这样就避免了不断调用fork()而产生无线子孙。
2.父子进程之间是互相平等的:他们的执行次序是随机的(并发执行),除非要使用特殊机制来同步他们,否则无法判断他们的运行顺序。
3.父子进程之间是相互独立的:由于子进程完整的复制了父进程的内存空间,因此从内存的角度来看他们是相互独立的、互不影响的。
[2]exec簇函数
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...); ---->从PATH环境变量中查找文件并执行
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[]);
参数:
path:即将被加载执行的ELF文件或脚本的路径
file:即将被加载执行的ELF文件或脚本的名字
arg:以列表方式罗列的ELF文件或脚本的参数
argv:以数组方式组织及的ELF文件或脚本的参数
envp:用户自定义的环境变量数组
返回值:
成功--->不返回
失败--->-1
备注:
l--->意味着参数以列表(list)的方式提供
v--->意味着参数以矢量(vector)数组的方式提供
p--->意味着会利用环境变量PATH来寻找指定的执行文件
c--->意味着用户提供自定义的环境变量
[3]退出本进程
1.void exit(int status);
2.void _exit(int status);
参数:
status:子进程的退出值
备注:
1.如果子进程正常退出,则status一般为0,异常退出,则status一般为非0
2.exit()退出时,会自动冲洗(flash)标准IO总残留的数据到内核,如果进程注册了“退出处理函数”,还会自动执行这些函数。
_exit()直接退出,不会执行注册函数atexit(3),所以一般使用exit。
int atexit(void (*function)(void));
参数:函数指针 (返回值void 参数void)
作用:进程退出时会调用 function指向的函数
[4]等待子进程
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
参数:
pid:回收的子进程的PID , 如果为-1 表示回收所用子进程
status:保存回收的状态值(exit的退出值)
options:一般为0(阻塞等待子进程退出)
返回值:
成功:子进程PID
失败:-1
备注:
pid_t wait(int *status); --->等价于waitpid(-1, &status, 0);
处理子进程退出状态值的宏
2 管道
2.1 管道的概念
管道分为无名管道和有名管道。
两者的区别:
- 有名管道有文件名,可以使用ls去查看。无名管道没有文件名。
- 无名管道的生存周期是跟随进程的,所以相关的所有进程退出或者无名管道的相关文件描述符被关闭,无名管道也跟随消失。
- 无名管道的读写具有方向性,具有两个文件描述符,一个读一个写。有名管道可以像普通文件一样去操作,但是多了一个阻塞性。
为什么pipe和fifo不能使用lseek()定位?
因为他们的数据不像普通文件那样按照块的方式存放在硬盘、flash等块设备上,而更像一个看不见源头的水龙头,无法定位。
2.2 无名管道PIPE
[1]无名管道的创建
#include <unistd.h>
int pipefd[2];
int pipe(int pipefd[2]);
参数:
pipefd:具有2个int型数据的数组,分别是pipefd[0]读、pipefd[1]写端描述符
返回值:成功:0 失败:-1
无名管道的特征:
- 没有名字,因此无法使用open()
- 只能用于亲缘进程间(父子、兄弟、祖孙…)通信
- 半双工工作方式:读写端分离
- 写入操作不具有原子性,因此只能用于一对一的简单通信情形
- 不能使用lseek()来定位
pipe.c
int fd[2];
//创建无名管道
if(-1 == pipe(fd))
{
perror("pipe");
exit(-1);
}
//创建线程
pid_t pid = fork();
if(0 == pid)
{
puts("child");
char *s = "hello,zix";
write(fd[1],s,strlen(s));//通过写端fd[1]将数据写入pipe
}
if(pid > 0)
{
puts("parent");
char buf[100];
memset(buf,0,sizeof(buf));
read(fd[0],buf,sizeof(buf));//通过读端fd[0]将数据读出来
printf("from child:%s\n",buf);
}
//关闭文件描述符
close(fd[0]);
close(fd[1]);
2.3 有名管道FIFO
[1]使用命令创建
gec@ubuntu:~$ mkfifo fifo_file
注:不能在与windows的共享路径下创建
[2]使用函数创建
int mkfifo(const char *pathname, mode_t mode);
参数:
参数1:文件路径+名字
参数2:模式---> O_CREAT | 0666
返回值:成功:0 失败:-1
有名管道的特征:
- 有名字,存储于普通文件系统中
- 任何具有相应权限的进程都可以使用open()来获取FIFO的文件描述符
- 跟普通文件一样,使用read()/write()来读写
- 跟普通文件不同,不能使用lseek()来定位
- 具有写入原子性,支持多写者同时进行写擦欧总而数据不会互相践踏
- 最先被写入FIFO的数据,最先被读出来
fifo.c
#define FIFO_PATH "/home/gec/fifo_file"
//1.创建有名管道
if(access(FIFO_PATH,F_OK))//判断是否存在
{
int ret = mkfifo(FIFO_PATH,O_CREAT | 0666);
if(-1 == ret)
{
perror("create fifo");
exit(-1);
}
}
//2.打开
int fd = open(FIFO_PATH,O_RDWR);
if(-1 == fd)
{
perror("open fifo");
exit(-1);
}
//3.读写
char buf[100];
while(1)
{
memset(buf,0,sizeof(buf));
#ifdef RD
sleep(6);
read(fd,buf,sizeof(buf));
printf("%s\n",buf);
#elif defined(WR)
fgets(buf,sizeof(buf),stdin);
write(fd,buf,strlen(buf));
#endif
}
//4.关闭
close(fd);
3 信号
2.1 信号的概念
信号是一种特殊的IPC,大部分的信号时异步的。一般情况下,进程什么时候收到信号,收到怎么的信号都是无法事先预料的。
分析:
[1] 前面的31个信号都有特殊的名字,这些信号都是从UNIX系统继承下来的,也被称为“不可靠信号”。
特征:1.非实时信号不排队,信号的响应可以互相嵌套
2.如果目标进程没有及时响应非实时信号,那么随后到达的信号将会被丢弃
3.每一个非实时信号都对应着一个系统事件,当这个事件发生时,将产生这个信号
4.如果进程的挂起信号中含有实时和非实时信号,那么进程会优先响应实时信号,
并且会按照从大到小依次响应,而非实时信号没有固定的顺序。
[2]后面的3个信号(34-64)是Liunx系统新增的实时信号,也被称为“可靠信号”。
特征:1.实时信号的响应次序按照接收顺序排队,不嵌套
2.即使相同的实时信号被同时发送多次,也不会被丢弃,而会依次挨个响应
3.实时信号没有特殊的系统事件与之对应
3.2 相关API
[1] 向指定进程或者进程组,发送一个指定的信号
[命令]:
kill -<信号值> <PID>
killall -<信号值> ./main
[函数]:
int kill(pid_t pid, int sig);
参数1:小于-1---->信号将被发送给组ID等于-pid的进程组里面的所有进程
-1---->信号将被发送给所有进程
0---->信号将被发送给与当前进程同一个进程组内的所有进程
大于0----->信号将被发送给PID等于pid的指定进程
参数2:要发送的信号
返回值:成功0 失败-1
[2] 捕捉一个指定的信号
sighandler_t signal(int signum, void (*sighandler_t)(int));
参数1:要捕捉的信号
参数2:函数指针--->接收到信号signum
SIG_DEF--->默认行为
SIG_IGN--->忽略
返回值:成功--->最近一次调用该函数时参数2的值
失败--->SIG_ERR
[3] 自己给自己发送一个指定的信号
int raise(int sig);
参数:要唤醒的信号
返回值:成功0 失败非0
[4] 将本进程挂起,知道接收到一个信号
int pause(void);
返回值: -1--->收到非致命信号或者已经被捕捉到的信号
不返回--->收到致命信号导致进程异常退出
备注:pause()响应函数返回值偶,随后返回。
若想在一个进程中持续接收信号,可以在死循环中写。
信号集操作函数的接口规范:
1.将信号集清空
int sigemptyset(sigset_t *set);
2.将所有的信号添加到信号集中
int sigfillset(sigset_t *set);
3.将指定的一个信号添加到信号集中
int sigaddset(sigset_t *set,int signum);
4.将指定的一个信号从信号集中删除
int sigdelset(sigset_t *set,int signum);
5.判断一个指定的信号是否被信号集包含
int sigismember(const sigset_t *set,int signum);
参数set:信号集
[5] 定时发送alarm信号给进程自己
unsigned int alarm(unsigned int seconds);
参数:定时的秒数
备注:若定时到了,系统会自动发送14号信号给调用alarm函数的进程
kill.c
void wait_fun(void)
{
int status,ret;
ret = wait(&status);
if(ret == -1)
{
perror("wait error");
}
else if(WIFEXITED(status))
{
printf("child PID:%d , exit:%d\n",ret,WEXITSTATUS(status));
}
else if(WIFSIGNALED(status))
{
printf("child PID:%d , sig:%d\n",ret,WTERMSIG(status));
}
}
int main(int argc,char *argv[])
{
pid_t pid = fork();
if(pid == 0)
{
puts("child");
sleep(3);
kill(getppid(),9);
puts("send 9#sig");
while(1) sleep(1);
}
else
{
puts("parent");
wait_fun();
puts("child is exit");
}
return 0;
}
4 IPC对象
system-V IPC是消息队列、共享内存的总称,也被叫做IPC对象。
IPC对象一旦被创建,就存在于内核中,属于内核管理。
[1] 获取一个当前未用的IPC的键值
key_t ftok(const char *pathname, int proj_id);
参数1:路径名
参数2:标识符 (范围为1-255)
返回值:成功键值 失败-1
IPC对象命令操作
[查看]ipcs
-m 共享内存
-q 消息队列
-s 信号量
-a 全部IPC对象
[删除]ipcrm
ipcrm [ -M key | -m id | -Q key | -q id | -S key | -s id ]
4.1 消息队列msg
带有标识的消息的发送与接受的功能的对象。
4.1.1 消息队列的使用方法
[1] 发送者
- 获取消息队列的ID
- 将数据放入一个附带有标识的特殊的结构体,发送给消息队列
[2]接收者
- 获取消息队列的ID
- 将指定标识的消息读出
当发送者和接收者都不再使用消息队列时,及时删除并释放资源。
4.1.2 相关API
[1] 获取消息队列的ID
int msgget(key_t key, int msgflg);
参数1:消息队列的键值
参数2:IPC_CREAT---->如果key对应的msg不存在,则创建该对象
IPC_EXCL----->如果key对应的msg已存在,则报错
mode--------->msg的访问权限
返回值:成功-->该消息队列的ID 失败-1
备注:如果key指定为IPC_PRIVATE,则自动产生一个随即未用的新键值
只有读写权限,没有执行权限。0777只能赋rw-(0666)
[2] 消息的发送
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数1:发送消息的消息队列ID
参数2:发送的数据的存储区域指针
参数3:发送的数据的大小
参数4:不设置特殊方式为0
IPC_NOWAIT、MSG_EXCEPT、MSG_NOERROR
返回值:成功0 失败-1
备注:struct msgbuf
{
long mtype;//消息的biaoshi
char mtext[20];//消息的正文
};
[3] 消息的接收
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz,
long msgtyp, int msgflg);
参数4:要接收的消息的标识
[4] 设置或者获取消息队列的属性
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数2:函数执行命令
IPC_STAT---->查看属性
IPC_SET----->修改属性
IPC_RMID---->删除对象
IPC_INFO---->查看IPC信息
参数3:相关信息结构体缓冲区
返回值:成功0 失败-1
msg.c
struct msgbuf
{
long mtype;
char mtext[20];
};
#define TYPE 10
int main(int argc,char *argv[])
{
int msgid = msgget(ftok(".",1),IPC_CREAT | 0666);
if(-1 == msgid)
{
perror("msgget");
exit(-1);
}
struct msgbuf Mbuf;
int ret;
#ifdef SND
Mbuf.mtype = TYPE;
while(1)
{
fgets(Mbuf.mtext,sizeof(Mbuf.mtext),stdin);
ret = msgsnd(msgid,&Mbuf,strlen(Mbuf.mtext),0);
if(-1 == ret)
perror("msgsnd");
if(strncmp(Mbuf.mtext,"over",4) == 0) break;
}
#elif defined(RCV)
while(1)
{
ret = msgrcv(msgid,&Mbuf,sizeof(Mbuf),TYPE,0);
if(-1 == ret)
perror("msgrcv");
printf("msgrcv recive %dbytes:%s\n",ret,Mbuf.mtext);
if(strncmp(Mbuf.mtext,"over",4) == 0) break;
}
#endif
#ifdef RMID
msgctl(msgid,IPC_RMID,NULL);
#endif
return 0;
}
4.2 共享内存msg
内存共享是效率最高的IPC,因为他抛弃了内核这个代理人,直接将一块裸露的内存放在数据传输的进程面前。
当进程p1向虚拟内存中区域1写入数据时,进程p2同时在其虚拟内存的区域2中可以看到这些数据,中间过程无转发,效率极高。
4.2.1 共享内存的使用方法
步骤:
- 获取共享内存对象的ID
- 将共享内存映射至本进程虚拟内存空间的某个区域
- 当不再使用时,解除映射关系
- 当没有进程再需要这块共享内存时,删除它。
4.2.2 相关API
[1] 获取共享内存的ID
int shmget(key_t key, size_t size, int shmflg);
参数1:消息队列的键值
参数2:共享内存的尺寸
参数3:IPC_CREAT---->如果key对应的msg不存在,则创建该对象
IPC_EXCL----->如果key对应的msg已存在,则报错
mode--------->msg的访问权限
返回值:成功-->该共享内存的ID 失败-1
备注:如果key指定为IPC_PRIVATE,则自动产生一个随即未用的新键值
[2] 对共享内存进行映射
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数1:消息队列ID
参数2:需要映射的首地址,设置为NULL系统自动选择合适的
参数3:一般设置为0,SHM_RDONLY为只读
返回值:成功-->映射的首地址 失败-->(void *)-1
备注:共享内存只能以只读、可读写方式映射,不能以只写的方式映射。
[3]对共享内存解除映射
int shmdt(const void *shmaddr);
参数1:需要解除映射的首地址
返回值:成功0 失败-1
[4] 获取或者设置共享内存的相关属性
int shmctl(int shmid, int cmd, struct msqid_ds *buf);
shm.c
unsigned long const SIZE = 100;
int shmid = shmget(ftok(".",1),SIZE,IPC_CREAT | 0666);
if(-1 == shmid)
{
perror("shmget");
exit(-1);
}
char *shmaddr = shmat(shmid,NULL,0);
if((void *)-1 == shmaddr)
{
perror("shmat");
exit(-1);
}
char buf[100];
#ifdef SND
while(1)
{
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
memcpy(shmaddr,buf,strlen(buf));
}
#elif defined(RCV)
while(1)
{
memset(buf,0,sizeof(buf));
memcpy(buf,shmaddr,sizeof(buf));
puts(buf);
sleep(3);
}
#endif
return 0;
4.3 信号量sem
信号量SEM全称Sempphore,信号灯。
4.3.1 信号量概念
Linux常见的信号量有三种:ststem-V信号量、POSIX有名信号量、POSIX无名信号量。
- P:申请资源(-1) V:释放资源(+1)
- P、V操作的特征:它们是原子性的
4.3.2 相关API
[1] 获取信号量ID
int semget(key_t key, int nsems, int semflg);
参数1:信号量的键值
参数2:信号量元素的个数
参数3:IPC_CREAT---->如果key对应的msg不存在,则创建该对象
IPC_EXCL----->如果key对应的msg已存在,则报错
mode--------->msg的访问权限
返回值:成功-->该信号量ID 失败-1
[2] 对信号量进行P、V操作
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数1:信号量ID
参数2:信号量操作结构体数组
struct sembuf{
unsigned short sem_num; /*信号量序号*/
short sem_op; /*PV操作*/
short sem_flg; /*操作选项 一般为0*/
};
参数3:结构体数组元素个数
返回值:成功0 失败-1
备注:当sem_op > 0时:进行V操作,即信号量元素的值将会加上sem_op
当sem_op = 0时:进行等零操作
当sem_op < 0时:进行P操作,即信号量元素的值将会减去sem_op
[3] 获取或者设置信号量的相关属性
int semctl(int semid, int semnum, int cmd, ...);
参数1:信号量ID
参数2:信号量元素序号(数组下标)
参数4:联合体的值val
union semun {
int val; /*当cmd为SETVAL时使用*/
struct semid_ds *buf; /*当cmd为IPC_STAT或IPC_SET时使用*/
unsigned short *array; /*当cmd为GETALL或SETALL时使用*/
struct seminfo *__buf; /*当cmd为IPC_INFO时使用*/
}
sem.c
union semun {
int val; /* SETVAL*/
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf;
};
int main(int argc,char *argv[])
{
/****************共享内存***********************/
size_t size = 100;
int shmid = shmget(ftok(".",1),size,IPC_CREAT|0666);
if(shmid == -1)
{
perror("shmget");
return -1;
}
char *shmaddr = shmat(shmid,NULL,0);
if(shmaddr == (void*)-1)
{
perror("shmat");
return -1;
}
/***********************************************/
/****************信号量初始化*******************/
int semid = semget(ftok(".",2),1,IPC_CREAT|IPC_EXCL|0666);
if(semid == -1)
{
if(errno != EEXIST)
{
perror("semget");
return -1;
}
else
{
semid = semget(ftok(".",2),1,0666);
}
}
else
{
union semun SemSet;
SemSet.val = 1;
semctl(semid,0,SETVAL,SemSet);
}
/***********************************************/
/***************信号量读写**********************/
char buf[100];
struct sembuf SemSet;
SemSet.sem_num = 0;
SemSet.sem_flg = 0;
#ifdef SND
while(1)
{
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
memcpy(shmaddr,buf,strlen(buf));
SemSet.sem_op = 1;
semop(semid,&SemSet,1);
}
#elif defined(RCV)
while(1)
{
SemSet.sem_op = -1;
semop(semid,&SemSet,1);
memset(buf,0,sizeof(buf));
memcpy(buf,shmaddr,sizeof(buf));
puts(buf);
}
#endif
/***********************************************/
return 0;
}