一些问题
- 什么是进程?
进程就是正在运行的程序, 是动态的. 是操作系统分配资源的最小单位. - 什么是程序?
程序是存储在存储介质上的经过编译的可执行二进制文件, 是静态的.
一些概念
时间片
时间片: cpu分配运行时间的单位, 它是CPU在进程上运行的前提.
CPU会保证每一个进程都会有一定数量的时间片, 轮巡地在各个进程之间切换.
由于CPU的运行速度非常快, 其轮巡的速度也远远在我们可以察觉的速度之上
进程ID
每个进程都有唯一的标识符,这个标识符就是进程ID。简称为“PID"。
进程A与进程B之间交流的过程成为“进程间的通信”。进程并不能通过应用层直接交流,通过Linux内核进行交流。在内核中创造一个对象,利用对象进行交流。所以进程通信都是基于文件IO进行的。如通过open/close
打开/关闭交流对象,通过read
获取信息,通过write
发送信息。
像一些特殊的对象,如管道等,一般都使用文件IO进行而不是标准IO。
进程的三种基本状态
- 就绪态:除了CPU以外的其它资源全部准备好了。可以变成执行态,可以由执行态和阻塞态转变而来。
- 执行态:CPU正在处理执行这个进程。可以转变成阻塞态、就绪态,只能由就绪态转变而来。
- 阻塞态:进程再等待其他资源准备好。可以转变成就绪态,只能由执行态转变而来。
进程的控制:
进程的创建
fork
函数:
头文件:unistd.h
函数原型:pid_t fork(void)
返回值:fork函数返回:
- 父进程中,返回子进程的PID
- 子进程中,返回0
- 出现错误,返回一个负值
什么是父进程、子进程?进程1中创建了进程2,那么它们之间存在的创建于被创建的关系就是父子关系。也即进程1是进程2的父进程;进程2是进程1的子进程。
#include<stdio.h>
#include<unistd.h>
int main(void)
{
pid_t pid;
pid=fork();
if(pid<0)
{
printf("process create failed!\n");
}
else if(pid == 0)
{
printf("This is child process!\n");
}
else
{
printf("This is parent process!Its child PID=%d\n",pid);
}
return 0;
}
编译运行上述文件,根据PID的输出就可以判断进程创建是否成功、当前进程是子进程还是父进程。
获取进程的PID
函数:getpid()
和getppid()
分别可以获取当前、父进程的PID。所以可以修改上面的c文件以获取子进程时的父进程以及自身PID,修改如下:
#include<stdio.h>
#include<unistd.h>
int main(void)
{
pid_t pid;
pid=fork();
if(pid<0)
{
printf("process create failed!");
}
else if(pid == 0)
{
printf("This is child process!");
printf("PID=%d parent PID=%d\n",getpid(),getppid());
}
else
{
printf("This is parent process!");
printf("PID=%d Its child PID=%d\n",getpid(),pid);
}
return 0;
}
~
子进程中的父进程PID应该和父进程中的PID相同。测试上述文件结果如下:
为什么会输出两行结果?因为在创建进程之后,子进程在创建的位置复制父进程的资源后从fork语句下执行。与此同时父进程也继续从fork语句往下处理,所以打印出了两行结果:
父子进程的执行顺序
父子进程的执行顺序是不定的,父子进程之间也是竞争CPU的。所以谁先抢占到CPU谁就先执行(父慈子孝),没有固定的谁先谁后。
ps命令和kill命令
ps命令
ps命令可以列出系统中当前运行的那些进程。
Usage: ps [options]
功能:用来显示当前进程的状态
常用参数:
aux 注意没有-符号
a 显示关联终端所有进程(可以与用户进行交互的)
u 显示进程的归属用户,相关内存的使用情况
x 显示的a参数涉及的进程的补集(ax显示所有不关联终端的进程)
To see every process on the system using BSD syntax:
ps ax
ps axu
TTY是关联的终端。另外aux显示的进程太多了,如果我们需要特别地查找一些进程,该如何使用呢?这会在后面的管道中进行讲解,这里举一个例子(查找/usr/sbin/vmtoolsd相关的进程):
ps aux | grep /usr/sbin/vmtoolsd
kill命令
kill命令用来杀死进程
kill -9(SIGKILL) PID
这里-9或者SIGKILL是一种“信号”
可以用kill -l 查询总共有哪些信号
在后面的信号通信中会进一步讲解
假设我们在当前路径下有一个./a.out
进程,会一直循环无法退出。我们新开一个控制台,用如下命令查找进程的PID,并用kill命令杀死该进程:
ps aux | grep a.out
这里的第一行就是我们循环的程序,第二列就是PID号(第一列被模糊处理了)3220
。我们用kill
命令杀死该进程:
kill -9 3220
孤儿进程和僵尸进程
孤儿进程:父进程结束以后,子进程还没有结束,这个子进程就叫做“孤儿进程”。
将是进程:子进程结束后,父进程还在运行且不去释放进程进程控制块,这个子进程就叫做“僵尸进程”
孤儿进程
孤儿进程会被PID为1的Init
进程领养,也就是子进程的PPID变成1(对于Ubuntu系统,会用别的PID对应的进程作为Init进程)。我们可以做如下实验:
#include<unistd.h>
#include<stdio.h>
int main(void)
{
pid_t pid;
pid=fork();
if(pid<0)
{
printf("fork error!\n");
}
else if(pid==0)
{
printf("1st PPID=%d\n",getppid());
sleep(5);//子进程先sleep 5秒,等待父进程结束
printf("2nd PPID=%d\n",getppid());
}
else
{
printf("1st Father,PID=%d\n",getpid());
}
return 0;
}
执行上述程序,结果如下:
可以发现子进程最开始的PPID和最后的PPID并不相同,但是第二个PPID也不为1!我们查找一下这个进程:
ps aux | grep 868
这也印证了Ubuntu系统的Init
进程的PID是其他的进程(这里是systemd --user
)。
僵尸进程
注意:父进程要没有时间去释放进程控制块(父进程的作用之一就是释放子进程的相关资源,这就是为什么孤儿进程需要被领养),子进程才是僵尸进程。
我们做如下实验:
#include<unistd.h>
#include<stdio.h>
int main(void)
{
pid_t pid;
pid=fork();
if(pid<0)
{
printf("fork error!\n");
}
else if(pid>0)
{
while(1);//让父进程一直运行,没有时间去释放进程控制块。
}
else
{
printf("this is child proc\n");
}
return 0;
}
结果自然是卡死状态,我们使用ps命令查找相关进程如下:
第一行是正在运行的父进程,他的状态是R+
(运行);第二行是子进程,它的状态是Z+
(僵尸),后面的<defunct>
表示失灵的,死的。
我们可以使用kill
命令杀死正在运行父进程,子进程就变成了
进程间的通信的几种方法
管道通信
分为有名管道和无名管道。
无名管道
特点:只能实现有亲缘关系进程之间的通信。
//原型:
//int pipe(int pipefd[2]);
//int pipe2(int pipefd[2], int flags);
//pipefd[0] refers to the read end of the pipe.
//pipefd[1] refers to the write end of the pipe.
//就是说int数组的第0位是管道读末端的文件描述符,第1位是管道写末端的文件描述符
//创建成功,返回0;否则返回-1;
我们以如下例子进一步说明:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<fcntl.h>
int main(void)
{
pid_t pid;
int pipefd[2];
int flag;
char buf[64]={0};
long count=0;
int tmp;
//注意,如何保证父子进程操作的是同一个管道?
//应该在fork函数创建之前,调用pipe函数,然后子进程创建的时候会自动复制父进程的资源。
flag=pipe(pipefd);
if(flag==0)
{
printf("read fd:%d\n",pipefd[0]);
printf("write fd:%d\n",pipefd[1]);
}
else
{
printf("pipe create error!\n");
}
pid = fork();
int status;
if(pid<0)
{
printf("fork error!\n");
}
else if(pid==0)
{
//进程没有使用写端,也不要先关闭,否则read失去阻塞性质,为什么?
//close(pipefd[1]);
count=read(pipefd[0],buf,32);
printf("count=%ld\n",count);
close(pipefd[1]);//可以放在read后面关闭写端
if(count>0)
{
printf("recv:%s\n",buf);
}
close(pipefd[0]);//进程用完了读端,可以关闭了
exit(0);
}
else
{
count=write(pipefd[1],"Hello,this is father proc\n",27);
close(pipefd[0]);//进程没有使用读端,也不可以先关闭
close(pipefd[1]);//进程用完了写端,可以关闭了
wait(&status);
exit(0);
}
return 0;
}
运行上述程序,结果为:
父进程成功发送,子进程成功接收并将其打印。注意,如果注释掉父进程端的write
命令,会导致子进程的read
阻塞。前提是子进程在read之前没有关闭写端。
有名管道
有名管道:可以实现没有任何关系的进程之间的通信。
何为有名无名?就是文件系统中的管道是否有文件名。所以需要创建一个管道类型的文件。
#命令行创建
mkfifo [OPTION]... NAME...
创建有名管道文件,文件类型为p,大小为0,不占磁盘空间。
//C源文件中创建
//原型:
int mkfifo(const char *pathname, mode_t mode);
//其中pathname是文件路径名称,mode控制文件权限(与文件掩码作与非得到真正的文件权限)
下面是有名管道C文件创建和有名管道写数据示例:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<fcntl.h>
#include<unistd.h>
int main(int argc,char **argv)
{
int ret;
int fd;
if(argc<2)
{
printf("Usage: %s <fifo name>\n",argv[0]);
return -1;
}
//access函数,如果第一个参数对应的文件存在,就直接进入打开文件,否则返回-1
if(access(argv[1],F_OK)==-1)
{
ret=mkfifo(argv[1],0666);
if(ret == -1)
{
printf("mkfifo error!\n");
return -2;
}
printf("mkfifo succeed!\n");
}
fd=open(argv[1],O_WRONLY);
while(1)
{
sleep(3);
write(fd,"hello",5);
}
close(fd);
return 0;
}
下面是有名管道的读数据示例
#include<stdio.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
int main(int argc,char **argv)
{
char buf[32]={0};
int fd;
if(argc < 2)
{
printf("Usage: %s <fifo name>\n",argv[0]);
return -1;
}
fd=open(argv[1],O_RDONLY);
while(1)
{
sleep(1);
read(fd,buf,32);
printf("buf is %s\n",buf);
memset(buf,0,sizeof(buf));
}
close(fd);
return 0;
}
分别在两个终端上执行(假设文件1编译出write
,文件2编译出read
mkfifo ./pipe
./write pipe
./read pipe
结果是,执行读文件的控制台会每隔3秒输出一次buf is hello
,但是可以注意到如果使用ctrl+c
终止写的控制台,读控制台就会每个1秒输出一次buf is
,思考为什么read
阻塞性消失?
信号通信
包括信号的发送、接收(只要进程不终止,就可以接受信号)、处理。
什么是信号?
信号通信的过程
- 通信的发起进程告诉内核,通信的进程的PID
- 通信的发起进程告诉内核,通信的信号类型
- 另一方面,接受信号的进程会对接受的信号进行处理
信号的发送
//kill函数
#include<sys/types.h>
#include<signal.h>
int kill(pid_t,int sig);
//两个参数,一个是PID,第二个时信号的种类。
//功能是当前进程给PID对应的进程发送信号
//raise函数
#include<signal.h>
int raise(int sig);
//一个参数:信号的种类
//功能是进程给自己发送信号,所以等价于kill(getpid(),sig);
//alarm信号
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
//一个参数,定时时间(单位:秒)
//功能:定时,然后发送SIGALRM信号,效果是终止
raise函数
实验:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
#include<stdlib.h>
int main(void)
{
printf("before raise func!\n");
raise(9);
//由于信号9是SIGKILL,效果是终止,所以这个进程运行到raise函数后就把自己终止了
// 故而不会输出下面这句话
printf("after raise func!\n");
}
执行结果如下:
可以发现,结果非常像我们在shell中使用kill -9
命令的结果,实际上就是进程在接收到9信号后,处理的方式就是杀死自己。
kill函数
执行如下实验:
#include<stdlib.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
int main(int argc,char **argv)
{
pid_t pid;
int sig;
if(argc<3)
{
printf("Usage: %s <pid_t> <signal>\n",argv[0]);
return -1;
}
//atoi, atol, atoll - convert a string to an integer
//把字符串变为其内容的整型
sig=atoi(argv[2]);
pid=atio(argv[1]);
kill(pid,sig);
return 0;
}
如上,这实现了类似于shell中的kill
命令的C源程序,用法也相同。假设同路径下有一个./test
文件正在执行,且不会自己退出,其PID为9510
。那么我们使用如下命令杀死该进程(假设例程对应的可执行文件名为killFun
:
./killFun 9510 9
#效果就等于: kill -9 9510
#这里提一下,按住键盘上的ctrl+c就是发送信号SIGINT给进程
alarm函数
就好似一个闹钟,到了设定值就发送信号SIGALRM
,如果该进程不去捕获该信号,就会终止该进程。
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
int main(int argc,char **argv)
{
int i;
alarm(3);
while(1)
{
sleep(1);
i++;
printf("i=%d\n",i);
}
return 0;
}
执行结果为:
和预期一样,只会打印两个i,打印第三个i之前3秒钟到了,会终止运行。
pause函数
#include <unistd.h>
int pause(void);
功能:等待信号
返回值:接收到信号后返回-1,否则就阻塞(休眠)。
信号的处理
信号处理的三种方式:默认、忽略、捕获
- 默认:大部分信号的处理方式,系统默认终止进程
- 忽略:不管信号
- 捕获:接受信号后,做想要做的事情。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数1:我们要处理的信号,可以在终端输入kill -l查看
参数2:处理的方式(可以选择系统默认、忽略、捕获,分别对应SIG_DFL,SIG_IGN,和自己写的函数)
一些例子:
signal(SIGINT,SIG_IGN);//忽略信号
signal(SIGINT,SIG_DFL);//系统默认操作,大部分的默认操作是终止该进程
signal(SIGINT,myfun);//接到SIGINT这个信号,执行我们自己写的myfun中的内容。
我们做如下实验:
//创建一个不会停止的进程,用于接收信号
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void fun(int a)
{
printf("a=%d\n",a);
}
int main()
{
__sighandler_t myfun=fun;
//如果接收到SIGINT信号(就是我们在终端按ctrl+c),就执行fun函数。
signal(SIGINT,myfun);
while(1)
{
sleep(3);
printf("process running!\n");
}
}
在执行上述文件编译得到的可执行文件时,按下ctrl+c
,结果如下,符合预期:
同时也可以发现,传入捕捉处理函数的值就是宏定义SIGINT
对应的值2。
共享内存
字面意思上来看,两个进程共享有同样一块内存。通过对该共享内存读写,即可实现进程间通讯。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
功能:创建一块共享内存
参数1:IPC_PRIVATE或者ftok函数的返回值
参数2:共享内存的大小(字节为单位),应该是内存一页体积的整数倍
参数3:权限,就是三组,可读写执行的1,2,4的组合。如0777,0666
返回值:成功则返回共享内存的标识符,失败返回-1。
#include <sys/ipc.h>
#include <sys/shm.h>
key_t ftok(const char *pathname, int proj_id);
功能:convert a pathname and a project identifier to a System V IPC key
也就是把一个文件名和一个工程标识符转换成一个System V风格的IPC key
参数1:文件路径以及文件名
参数2:ascii字符,所以范围是1~127
返回值:System V风格下的IPC key
使用IPC_PRIVATE来创建共享内存
进行如下实验:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
int main(void)
{
int shmid;
shmid=shmget(IPC_PRIVATE,1024,0777);
if(shmid < 0)
{
printf("shmget error!\n");
return -1;
}
else
{
printf("shmget succeed!\n");
printf("shmid=%d\n",shmid);
}
return 0;
}
运行上述程序后得到结果如下:
ipcs命令
在shell命令行使用命令ipcs -m
查看关于IPC空间的情况:
Usage:
ipcs [resource-option...] [output-option]
ipcs -m|-q|-s -i <id>
功能:Show information on IPC facilities.显示关于IPC空间的使用信息。
-m shared memory segments,共享内存段
-q message queues,信息队列
-s semaphores,信号标,信号灯
-a all(default),全部显示(这是不带参数时的默认选项)
结果如下:
这说明共享内存申请成功!。
ipcrm命令
Usage:
ipcrm [options]
ipcrm shm|msg|sem <id>...
功能:Remove certain IPC resources.移除某些IPC资源
-m 根据共享内存段的id删除共享内存
-M 根据共享内存段的key删除共享内存
我们使用命令ipcrm -m 52
删除之前申请的shmid=52
的共享内存。再使用ipcs -m
命令查看,可以发现申请的相应共享内存被移除。
使用ftok函数来创建内存
在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。如指定文件的索引节点号为65538,换算成16进制为 0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。
查询文件索引节点号的方法是: ls -i
Typically, a best-effort attempt combines the given proj_id byte, the lower 16 bits of the inode number, and the lower 8 bits of the device number into a 32-bit result.
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <stdio.h>
//#include <error.h>
int main(void)
{
int shmid;
key_t key;
//printf("./a.c\n");
key=ftok("./a.c",0x20);
//pathname 必须是存在的可读写文件的文件路径文件名
//IPC_CREAT: Create a new segment.
//If this flag is not used, then shmget() will find the segment associated with key
// and check to see if the user has permission to access the segment.
//在使用ftok命令时,shmget命令的第三个参数要或IPC_CREAT,否则是打开模式而不是创造模式,
// 若对应的key不存在以创建的共享内存,shmget就会返回-1,表示打开失败。
//printf("key=%x\n",key);
//shmid=shmget(IPC_PRIVATE,1024,0666);
shmid=shmget(key,1024,0666|IPC_CREAT);
//perror("errno=");
if(shmid < 0)
{
printf("shmget error!\n");
return -1;
}
else
{
printf("shmget succeed!\n");
printf("shmid=%d\n",shmid);
}
return 0;
}
执行上述文件,得到新的共享内存为:
可以发现,key值发生了改变,计算方式可以看ftok相应的功能解释。
地址映射
为了方便用户空间对共享内存的操作,往往用地址映射的方法,将共享内存映射到用户空间的某块内存,这样就不用每次都进入内核,速度快。这也是为什么共享内存是进程通信诸多方法中效率最高的。
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:将内核中的共享内存绑定到当前进程中的内存区域
参数1:共享内存的的标识符(shmid)
参数2:映射到的目标地址,如果是NULL,则系统自动完成映射,其他情况详见man 2 shmat
参数3:通常为0,表示共享内存为可读可写,或者为SHM_RDONLY,表示共享内存只读。
返回值:成功映射,则返回共享内存映射到进程中的地址,失败返回-1
//与shmat同头文件
int shmdt(const void *shmaddr);
功能:删除绑定进程中的共享内存(不会删除内核中的共享内存)
参数:共享内存映射后的地址
返回值:成功返回0,失败返回-1。
共享内存的删除
上面的地址映射提及了映射绑定的内存的删除,那么怎么删除内核中的共享内存呢?
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:控制管理System V的共享内存
用于查看时用的结构体:
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
用于设置时用的结构体:
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and
SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
用于查看INFO时的结构体:
struct shminfo {
unsigned long shmmax; /* Maximum segment size */
unsigned long shmmin; /* Minimum segment size;
always 1 */
unsigned long shmmni; /* Maximum number of segments */
unsigned long shmseg; /* Maximum number of segments
that a process can attach;
unused within kernel */
unsigned long shmall; /* Maximum number of pages of
shared memory, system-wide */
};
参数cmd有如下选项:
IPC_STAT :将制定shmid的共享内存的属性结构体放到shmid_ds中
IPC_SET :利用ipc_perm中的内容,修改可以修改的内容
IPC_RMID :删除shmid对应的共享内存
IPC_INFO :查询当前系统下的共享内存的一些信息
一个综合实例
我们尝试使用共享内存进行父子进程通信,涉及共享内存的打开,映射,读取,解绑定,删除内核中的共享内存(这里不执行删除内核共享内存,仅仅写出示例)。并假设已经存在一段共享内存,其key值为0x200628d6
:
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/shm.h>
#include<sys/ipc.h>
#include<string.h>
int main(void)
{
pid_t pid;
int shmid;
key_t key=0x200628d6;
char *s_addr=NULL;
char buf[32]={0};
shmid=shmget(key,0,0666);
pid=fork();
if(pid<0)
{
printf("fork error!\n");
return -1;
}
if(pid==0)
{
s_addr=shmat(shmid,NULL,0);
strncpy(s_addr,"Hello This is child",20);
shmdt(s_addr);
exit(0);
}
else
{
sleep(2);
s_addr=shmat(shmid,NULL,0);
printf("%s\n",s_addr);
shmdt(s_addr);
//删除内核中的共享内存,前提是该段共享内存已经全部解绑,没有映射绑定了。
//shmctl(shmid,SHM_RMID,NULL);
}
return 0;
}
消息队列
创建消息队列
创建消息队列:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数:第一个:和消息队列相关的key值
第二个:访问权限设置
我们可以发现,他的用法和之前的共享内存非常相似,只是没有size这个参数
因为消息队列是一个线形结构队列,可以一直往里面插入信号(这和我们的管道有点相似)
返回值:成功返回ID,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:控制内核中的消息队列,和shmctl用法相似
参数1:消息队列的ID
参数2:IPC_STAT,IPC_RMID,IPC_INFO,IPC_SET
参数3:用于接收信息、发送设置的结构体
我们举一个例子来进一步说明:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
int main()
{
int msgid;
key_t key;
key=ftok("./a.c",0x20);
msgid=msgget(key,0666|IPC_CREAT);
if(msgid<0)
{
printf("msgget error!\n");
return -1;
}
else
{
printf("msgget succeed\n");
printf("msgid=%d\n",msgid);
}
return 0;
}
执行上述文件,得到结果,显然创建成功:
利用消息队列收发信息
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:利用消息队列发送信息
参数1:消息队列的id
参数2:消息结构体指针
参数3:要发送的消息的数量(消息结构体中的char数组的长度)
参数4:消息发送模式的控制包括阻塞与否、
截断后剩余的信息丢失与否
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
我们做如下的收发实验:
//msgwrite.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//消息结构体是发送者自定义的
//第一个参数long mtype必须有
//第二个参数可以是任何类型(自然包括结构体),只需要和接收方约定好即可
struct msgbuf
{
long mtype;
char mtext[128];
};
int main(int argc,char **argv)
{
struct msgbuf msg;
int msgid=0;
msg.mtype=1;
int msgflg=0;
int tmp;
strncpy(msg.mtext,"Hello from msgque sender",24);
tmp=msgsnd(msgid,&msg,strlen(msg.mtext),msgflg);
if(tmp<0)
{
printf("msgsnd error!\n");
}
else
{
printf("msgsnd succeed!\n");
}
return 0;
}
//msgread.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//与发送方约定好的数据类型
struct msgbuf
{
long mtype;
char mtext[128];
};
int main(int argc,char **argv)
{
struct msgbuf msg;
int msgid=0;
int mtype=0;//表示接收队列中的所有数据
int msgflg=0;//表示数据截断处理方式
int tmp;
tmp=msgrcv(msgid,&msg,128,0,0);
if(tmp>=0)
{
printf("recv: %s\n",msg.mtext);
}
else
{
printf("msgrcv error!\n");
}
return 0;
}
先执行msgwrite
,后执行msgread
。结果如下,收发成功:
可以发现,发送信号后消息队列的属性发生了变化,消息数量增加,使用的字节数也增加了。读取之后,消息数量减少,占用字节数也变回0。
信号量(灯,标):semaphore
信号量用于保护共享资源,也存在于内核中。下面是百度百科的定义:
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。
所以说信号灯就像一个计数器,描述共享资源的占用情况,便于协调多个进程之间的资源使用。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
函数功能:获取信号量的ID
参数1:想要创建/打开的信号量的key值
参数2:信号量集中的信号量数量
参数3:信号量的权限设置
int semctl(int semid, int semnum, int cmd, ...);
函数功能:根据参数cmd,对信号量进行操作
参数1:信号量ID
参数2:要操作的信号量在信号量集中的序号
参数3:信号量的操作类型选择,IPC_STAT,IPC_SET,IPC_RMID,SETVAL。
SETVAL就是设置信号灯的计数值
参数4:根据参数三,该参数可以没有或者执行相应的操作。使用union semun arg来设置或者存储
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
int semop(int semid, struct sembuf *sops, size_t nsops);
功能:操作信号量,进行对公共资源占用情况的标记
参数1:信号量ID
参数2:信号量结构体数组
参数3:要操作信号量的数量
struct sembuf
{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}
对于sem_op,可正可负可为0
1. 为t>0的时候标记信号灯次数增加t
2. 为t<0的时候,如果当前的semval>=t,则信号灯次数立刻减少t;
否则,如果sem_flg没有设置IPC_NOWAIT,该函数阻塞直到semval>=t。
3. 为0的时候就表示该函数阻塞,直到信号灯变成0。
对于sem_flg,0表示阻塞,IPC_NOWAIT表示非阻塞
进行如下实验:
//利用信号灯,保证父子进程的执行顺序
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
union semun {
int val; /* Value for SETVAL */
//本次示例中,只用到了val,所以该联合体下面的三个联合成员可以不写
//struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
//unsigned short *array; /* Array for GETALL, SETALL */
//struct seminfo *__buf; /* Buffer for IPC_INFO
// (Linux-specific) */
};
/*
//这个结构体已经被定义过了,不是由用户定义的
struct sembuf
{
unsigned short sem_num; // semaphore number
short sem_op; // semaphore operation
short sem_flg; // operation flags
};
*/
int main()
{
int semid;
int nsems=1;
int semflg=0666|IPC_CREAT;
union semun semun_union;
semun_union.val=0;
key_t key=ftok("./a.c",0x20);
semid=semget(key, nsems, semflg);
//由于申请的信号量集中的信号量数目为1,序号从0开始。所以要操作的信号量的索引为0
semctl(semid, 1, SETVAL, semun_union);
struct sembuf semopbuf={0,-1,0};
pid_t pid;
pid = fork();
if(pid<0)
{
printf("fork error\n");
}
else if(pid>0)
{
semop(semid, &semopbuf, 1);
//由于初始化信号灯的值semval为0,所以父进程会一直等待直到semval为1
//而子进程中会设置相关操作,所以一定会保证父进程在子进程之后运行
semopbuf.sem_op=1;
semop(semid, &semopbuf, 1); //要恢复一下信号灯的值为1
printf("This is parent\n");
}
else if(pid==0)
{
//由于子进程会sleep一秒,所以如果没有其他操作就一定会比父进程晚开始运行
//我们使用信号量来保证父进程在子进程后运行
sleep(1);
printf("This is son\n");
semopbuf.sem_op=1;
semop(semid, &semopbuf,1);//此举完成后,父进程才会开始运行。
}
return 0;
}
运行上述程序,结果如下,符合子进程一定先运行的预期:
socket通信
网络编程,出在同一个网络下的两个进程之间的通信