#include
int main()
{
printf(“The file is %s.\n”,__FILE __);
printf( “The date is %s.\n”, __DATE __ );
printf( “The time is %s.\n”, __TIME __);
printf( “This is line %d.\n”, __LINE __);
printf( “This function is %s.\n”, __FUNCTION __);
return 0;
}
字节对齐 — 详解点这里
(1)有些特殊的CPU只能处理4倍开始的内存地址
(2)如果不是整倍数读取会导致读取多次,效率低下
(3)数据总线为读取数据提供了基础
如果在编程的时候要考虑节约空间的话,基本的原则就是把结构中的变量按照类型大小从小到大声明,尽量减少中间的填补空间.
Nagle算法 — 百度百科
Nagle算法、TCP确认延迟机制、TCP_CORK — 参考博客
Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。 Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。
Nagle算法的规则:
(1)如果包长度达到MSS(Maximum Segment Size,用于在TCP连接建立时,收发双方协商通信时每一个报文段所能承载的最大数据长度,不包含文段头),则允许发送;
(2)如果该包含有FIN,则允许发送;
(3)设置了TCP_NODELAY选项,则允许发送;
(4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
(5)上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
注意:在一些交互性和实时性很强的场景下,不宜使用Nagle算法。
延迟ACK的问题,举个例子:
client端调用socket的write操作将一个int型数据(称为A块)写入到网络中,由于此时连接是空闲的(也就是说还没有未被确认的小段),因此这个int型数据会被马上发送到server端,接着,client端又调用write操作写入‘\r\n’(简称B块),这个时候,A块的ACK没有返回,所以可以认为已经存在了一个未被确认的小段,所以B块没有立即被发送,一直等待A块的ACK收到(大概40ms之后),B块才被发送。
为什么40ms之后才收到?
这是因为TCP/IP中不仅仅有nagle算法,还有一个TCP确认延迟机制 。当Server端收到数据之后,它并不会马上向client端发送ACK,而是会将ACK的发送延迟一段时间(假设为t),它希望在t时间内server端会向client端发送应答数据,这样ACK就能够和应答数据一起发送,就像是应答数据捎带着ACK过去。t大概就是40ms,这就解释了为什么’\r\n’(B块)总是在A块之后40ms才发出。
A向B发送了数据,B将不会立即回复ACK给A,而会在下面两种情况下才会回复ACK给A:
① B收到A发来的1个包,启动200ms定时器,还没超时,正好B要给对方A发点内容。于是对这个包的确认ack就跟着捎过去。这叫做“捎带发送”;
② B收到A发来的1个包,启动200ms定时器,等到200ms的定时器到点了(第二个包没来),于是对这个包的确认ack被发送。这叫做“延迟发送”;
TCP_CORK 选项
所谓的CORK就是塞子的意思,形象地理解就是用CORK将连接塞住,使得数据先不发出去,等到拔去塞子后再发出去。设置该选项后,内核会尽力把小数据包拼接成一个大的数据包(一个MTU,Maximum Transmission Unit,最大传输单元)再发送出去,当然若一定时间后(一般为200ms,该值尚待确认),内核仍然没有组合成一个MTU时也必须发送现有的数据。
CORK存在的缺陷:
TCP_CORK适用场景是在webserver,下载服务器(ftp的发送文件服务器),需要带宽量比较大的服务器。但是如果应用层程序发送小包数据的间隔不够短时,TCP_CORK就没有一点作用,反而失去了数据的实时性(每个小包数据都会延时一定时间再发送)。
注意:MSS加包头数据就等bai于MTU
Nagle算法与CORK算法区别
Nagle算法主要避免网络因为太多的小包(协议头的比例非常之大)而拥塞,Nagle算法关心网络拥塞问题,只要所有的ACK回来则发包(不关乎数据包大小)。Nagle不受用户socket的 控制,只能选择使用和禁用,满足上述五个Nagle规则就会发送数据。而***CORK算法则是为了提高网络的利用率***,使得总体上协议头占用的比例尽可能的小。如果发送的是几个小的数据包,则需要将数据包组合成一定大小,才可以发送出去。即使你是分散发送多个小数据包,你也可以通过使能CORK算法将这些内容拼接在一个包内,如果此时用Nagle算法的话,则可能做不到这一点。
谈谈守护进程和僵尸进程 — 参考博客
守护进程 — 参考博客
Daemon(精灵)进程, 是Linux中的后台服务进程, 通常独立于控制终端并且周期性地(一直都是活跃地)执行某种任务或等待处理某些发生的事件。
Linux后台的一些系统服务进程, 没有控制终端, 不能直接和用户交互. 不受用户登录和注销的影响, 一直在运行着, 他们都是守护进程. 如: 预读入缓输出机制的实现; ftp服务器; nfs服务器等
创建守护进程,最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader。
守护进程的特点
创建守护进程模型
#include
#include
#include
#include
#include
#include
#include
int main(int argc, const char * argv[]) {
// 创建一个会话
// 将子进程变为会长
pid_t pid = fork();
if (pid > 0) {
exit(1);
kill(getpid(), SIGKILL);
raise(SIGKILL);
abort();
}
else if (pid == 0) {
// 变为会长, 脱离控制终端, 变为守护进程
setsid();
// 改变工作目录
chdir("/home/zyb");
// 重设文件掩码
umask(0);
// 关闭文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
while (1);
}
return 0;
}
一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit, 它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位 置,记载该进程的退出状态等信息供其他进程收集。除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装 SIGCHLD 信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,用KILL发任何信号量也不能释放它。
如果这时父进程结束了, 那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这会造成未知的可怕结果,因为服务器上可能会出现无数ZOMBIE进程导致机器挂掉。
ps -elf 或者ps aux
快速记忆:
线程私有:线程栈,寄存器,程序寄存器
线程共享:堆,地址空间,全局变量,静态变量
进程私有:地址空间,堆,全局变量,栈,寄存器
进程共享:代码段,公共数据,进程目录,进程ID
线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易地实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。
进程拥有这许多共性的同时,还拥有自己的个性。有了这些个性,线程才能实现并发性。这些个性包括:
在线程的管理中,将线程共有的信息存放在进程控制块中(PCB,Process Control Block),将线程独有的信息存放在线程控制块中(TCB,Thread Control Block)。
那么如何区分哪些信息是共享的?哪些信息是独享的呢?
一般的评价标准是:如果某些资源不独享会导致线程运行错误,则该资源就由每个线程独享,而其他资源都由进程里面的所有线程共享。
进程是资源分配单元(内存,打开的文件,访问的网络),线程是CPU调度单位,CPU也是一种特殊的资源,要执行控制流需要的相关信息
进程拥有一个完整资源平台,而线程只独享必不可少的资源如寄存器和栈
线程同样具有就绪,阻塞和执行三种基本状态和转换关系
线程能减少并发执行的时空开销:
— 线程的创建时间、终止时间、同一进程内线程的切换时间都更短,因为进程要创建一些对内存和打开的文件的管理信息,而线程可以直接用所属的进程的信息,因为同一进程内的线程有同一个地址空间,同一个页表,所有信息可以重用,无失效处理。而进程要切页表,开销大,访问的地址空间不一样,cache,TLB等硬件信息的访问开销大。另外线程的数据传递不用通过内核,直接通过内存地址可以访问到,效率很高。
静态变量:变量的生存期是从程序开始到结束;
动态变量:在程序运行当中需要调用该变量时才为它分配内存;
寄存器变量:存在于寄存器中,用于需要高速存取数据的场合
标准流管道的读操作
#include
int main(){
FILE *fp;
fp = popen("/print", "r");
char buf[64] = {0};
fread(buf, sizeof(char), sizeof(buf), fp);
printf("buf = %s\n", buf);
return 0;
}
//print
#include
int main(){
printf("i am print\n");
return 0;
}
用FILE* 类型指针,可以定义一个单向管道,这里把print对标准输出的数据重定向到fp,用管道到buf中,在到标准输出打印出来。
标准流管道的写操作
#include
int main(){
FILE *fp;
//"w" 的方式启动新的进程
// fwrite写给文件流fp的 数据会传递给新进程的标准输入
//read进程可以读标准输入读到fwrite写进来的hello read
fp = popen("./read", "w");
fwrite("hello read", sizeof(char), 10, fp);
fclose(fp);
return 0;
}
//read
#include
int main(){
char buf[64] = {0};
read(0, buf, sizeof(buf));
printf("i am read\n");
printf("buf = %s\n", buf);
return 0;
}
无名管道
特点:
函数原型:
#include
int pipe(int fd[2]);
/*
管道在程序中用有一对文件描述符来表示,其中一个文件描述符有可读属性,
一个有可写属性。fd[0]是读,fd[1]是写。这是固定的。
*/
#include
int main(){
int fd[2] = {0};
int ret = pipe(fds);
ERROR_CHECK(ret, -1, "pipe");
printf("fds[0] = %d, fds[1] = %d\n", fds[0], fds[1]);
char buf[64] = {0};
write(fds[1], "hello", 5);
read(fds[0], buf, sizeof(buf));
printf("buf = %s\n", buf);
return 0;
}
除了上述的使用方法外,可以使用fork创建子进程来和父进程进行交互。
如图所示,父子进程可以相互通信,当然父子进程各自也可以自己本身内部进行通信。
#include
int main(){
int fds[2] = {0};
int ret = pipe(fds);
ERROR_CHECK(ret, -1, "pipe");
printf("fds[0] = %d, fds[1] = %d\n", fds[0], fds[1]);
char buf[64] = {0};
if(fork()){
write(fds[1], "hello", 5);
}else{
read(fds[0], buf, sizeof(buf));
printf("i am child, buf = %s\n", buf);
}
return 0;
}
上述代码表示了,父进程写,子进程读。
也可以在父子进程中关闭相应的 文件描述符,设置相应的读写属性,进行固定方向的通信。
有名管道
无名管道只能在亲缘关系的进程间通信大大限制了管道的使用,有名管道突
破了这个限制,通过指定路径名的范式实现不相关进程间的通信。
创建FIFO的函数原型:
#include
#include
int mkfifo(const char *pathname, mode_t mode);
//参数 pathname 为要创建的 FIFO 文件的全路径名;
//参数 mode 为文件访问权限
//如果创建成功,则返回 0,否则-1。
#include
#include
#include
#include
int main(int argc,char *argv[])//演示通过命令行传递参数
{
if(argc != 2){
puts("Usage: MkFifo.exe {filename}");
return -1;
}
if(mkfifo(argv[1], 0666) == -1){
perror("mkfifo fail");
return -2;
}
return 0;
}
删除FIFO的函数原型:
#include
int unlink(const char *pathname);
#include
main()
{
unlink("pp");//FIFO文件的名字
}
/*
对 FIFO 类型的文件的打开/关闭跟普通文件一样,都是使用 open 和 close 函数。如果打
开时使用 O_WRONLY 选项,则打开 FIFO 的写入端,如果使用 O_RDONLY 选项,则打开
FIFO 的读取端,写入端和读取端都可以被几个进程同时打开。
如果以读取方式打开 FIFO,并且还没有其它进程以写入方式打开 FIFO,open 函数将被
阻塞;同样,如果以写入方式打开 FIFO,并且还没其它进程以读取方式 FIFO,open 函数也
将被阻塞。
与 PIPE 相同,关闭 FIFO 时,如果先关读取端,将导致继续往 FIFO 中写数据的进程接
收 SIGPIPE 的信号。
*/
#include
#include
#include
#include
int main()
{
int fdFifo = open("MyFifo.pip",O_WRONLY); //1. 打开(判断是否成功打开略)
write(fdFifo, "hello", 6); //2. 写
close(fdFifo); //3. 关闭
return 0;
}
#include
#include
#include
#include
int main()
{
char szBuf[128];
int fdFifo = open("MyFifo.pip",O_RDONLY); //1. 打开
if(read(fdFifo,szBuf,sizeof(szBuf)) > 0) //2. 读
puts(szBuf);
close(fdFifo); //3. 关闭
return 0;
}
/*
然后 gcc –o write write.c
gcc –o read read.c
./write //发现阻塞,要等待执行./read
./read
在屏幕上输出 hello
*/
示例:基于管道的客户端服务器程序。
/*
程序说明:
1. 服务器端:
维护服务器管道,接受来自客户端的请求并处理(本程序为接受客户端发来的字符串,
将小写字母转换为大写字母。)然后通过每个客户端维护的管道发给客户端。
2. 客户端
向服务端管道发送数据,然后从自己的客户端管道中接受服务器返回的数据。
示例代码见下:
*/
//服务器端 server.c
#include
#include
#include
#include
#include
#include
#include
#include
typedef struct tagmag{
int client_pid ;
char my_data[512] ;
}MSG;
int main(){
int server_fifo_fd , client_fifo_fd ;
char client_fifo[256];
char *pstr;
MSG my_msg;
memset(&my_msg, 0, sizeof(MSG));
mkfifo("SERVER_FIFO_NAME",0777);
server_fifo_fd = open("./SERVER_FIFO_NAME",O_RDONLY);
if(-1 == server_fifo_fd){
perror("server_fifo_fd");
exit(-1);
}
int iret ;
while( (iret = read(server_fifo_fd , &my_msg ,sizeof(MSG))>0) ){
//从打开的SERVER_FIFO_NAME管道中读取数据,管道为空会在这里阻塞
//iret = read(server_fifo_fd , &my_msg ,sizeof(MSG));
pstr = my_msg.my_data ;
printf("%s\n",my_msg.my_data);//打印数据
while(*pstr != '\0'){// 数据变为大写
*pstr = toupper(*pstr);
pstr ++ ;
}
//打开客户端的FIFO,做一个回显操作
memset(client_fifo, 0, 256);
sprintf(client_fifo, "CLIENT_FIFO_%d", my_msg.client_pid);
client_fifo_fd = open(client_fifo, O_WRONLY);
if(client_fifo_fd == -1){
perror("client_fifo_fd");
exit(-1);
}
write(client_fifo_fd, &my_msg, sizeof(MSG));
printf("%s\n", my_msg.my_data);
printf("OVER!\n");
close(client_fifo_fd);
}
return 0 ;
}
//客户端代码:client.c
#include
#include
#include
#include
#include
#include
#include
typedef struct tagmag{
int client_pid ;
char my_data[512] ;
}MSG;
int main(){
int server_fifo_fd, client_fifo_fd;
char client_fifo[256]={0};
sprintf(client_fifo, "CLIENT_FIFO_%d", getpid());
MSG my_msg;
memset(&my_msg, 0, sizeof(MSG));
my_msg.client_pid = getpid();
server_fifo_fd = open("./SERVER_FIFO_NAME", O_WRONLY);
mkfifo(client_fifo, 0777);
while(1){
int n = read(STDIN_FILENO, my_msg.my_data, 512);//从标准输入读入数据
my_msg.my_data[n] = '\0';
write(server_fifo_fd, &my_msg, sizeof(MSG));
//从服务器接受回显的数据
client_fifo_fd = open(client_fifo, O_RDONLY);
//memset(&my_msg, 0, sizeof(MSG));
n = read(client_fifo_fd,&my_msg, sizeof(MSG));
my_msg.my_data[n]= 0;
write(STDOUT_FILENO, my_msg.my_data, strlen(my_msg.my_data) );
close(client_fifo_fd);
}
unlink(client_fifo);
}
信号量分为以下三种。
1、System V 信号量,在内核中维护,可用于进程或线程间的同步,常用于进程的同步。
2、Posix 有名信号量,一种来源于 POSIX 技术规范的实时扩展方案(POSIX Realtime
Extension),可用于进程或线程间的同步,常用于线程。
3、Posix 基于内存的信号量,存放在共享内存区中,可用于进程或线程间的同步。
为了获得共享资源进程需要执行下列操作:
(1)测试控制该资源的信号量。
(2)若信号量的值为正,则进程可以使用该资源。进程信号量值减 1,表示它使用了一个资源单位。此进程使用完共享资源后对应的信号量会加 1。以便其他进程使用。
(3)若信号量的值为 0,则进程进入休息状态,直至信号量值大于 0。进程被唤醒,返回第(1)步。
System V IPC 机制:信号量。
函数原型:
#include
#include
#include
//函数 semget 创建一个信号量集或访问一个已存在的信号量集。
//返回值:成功时,返回一个称为信号量标识符的整数,
//semop 和 semctl 会使用它出错时,返回-1.
int semget(key_t key,int nsems,int flag);
//参数 key 是唯一标识一个信号量的关键字,如果为IPC_PRIVATE(值为0,创建一
//>>个只有创建者进程才可以访问的信号量,通常用于父子进程之间;非0值的key(可以通
//>>过ftok函数获得)表示创建一个可以被多个进程共享的信号量;
//参数nsems指定需要使用的信号量数目。如果是创建新集合,则必须制定nsems。
//>>如果引用一个现存的集合,则将 nsems 指定为 0.
//参数 flag 是一组标志,其作用相当于文件的访问权限。此外,它们还可以与键值 IPC_CREAT
//>>按位或操作,以创建一个新的信号量。我们也可以通过 IPC_CREAT 和 IPC_EXCL
//>>标志的联合使用确保自己将创建出一个新的独一无二的信号量来,如果该信号量已经存
//>>在,就会返回一个错误。
//函数 semop 用于改变信号量对象中各个信号量的状态。返回值:成功时,
//返回 0;失败时,返回-1.
int semop(int semid, struct sembuf *sops, size_t num_sops);
//参数 semid 是由 semget 返回的信号量标识符。
//参数 sops 是指向一个结构体数组的指针。每个数组元素至少包含以下几个成员:
/*
struct sembuf{
short sem_num; //操作信号量在信号量集合中的编号,第一个信号量的编号是 0。
short sem_op; //sem_op成员的值是信号量在一次操作中需要改变的数值。
//通常只会用到两个值,一个是-1,也就是 p 操作,它等待信号量变为可用;
//一个是+1,也就是 v 操作,它发送信号通知信号量现在可用。
short sem_flg; //通常设为:SEM_UNDO,程序结束,信号量为semop调用前的值。
};
*/
//参数num_sops,为 sops 指向的 sembuf 结构体数组的大小。
//函数 semctl 用来直接控制信号量信息。函数返回值:成功时,返回 0;失败时,返回-1.
int semctl(int semid, int semnum, int cmd, …);
//参数 semid 是由 semget 返回的信号量标识符。
//参数 semnum 为集合中信号量的编号,当要用到成组的信号量时,从 0 开始。
//>>一般取值为 0,表示这是第一个也是唯一的一个信号量。
//参数 cmd 为执行的操作。通常为:
//>>IPC_RMID(立即删除信号集,唤醒所有被阻塞的进程)
//>>GETVAL(根据 semun 返回信号量的值,从 0 开始,第一个信号量编号为 0)
//>>SETVAL(根据 semun 设定信号的值,从 0 开始,第一个信号量编号为 0)
//>>GETALL(获取所有信号量的值,第二个参数为 0,将所有信号的值存入 semun.array中)
//>>SETALL(将所有 semun.array 的值设定到信号集中,第二个参数为 0)等。
//参数 … 是一个 union semun(**需要由程序员自己定义**),它至少包含以下几个成员:
/*
union semun{
int val; // Value for SETVAL
struct semid_ds *buf; // Buffer for IPC_STAT, IPC_SET
unsigned short *array; // Array for GETALL, SETALL
};
*/
示例:有亲缘关系的信号量进程间通信
#include
#include
#include
#include
#include
#include
#include
#include
union semun{ //必须重写这个共用体
int val;
struct semid_ds *buf;
unsigned short* array;
};
int main(){
int semid = semget(IPC_PRIVATE, 1, 0666|IPC_CREAT);
//创建信号量集,IPC_PRIVATE用于父子进程
if(semid == -1){
perror("semget error");
exit(-1);
}
if(fork() == 0){ //表示子进程
struct sembuf sem; //定义信号量结构体
memset(&sem, 0, sizeof(struct sembuf));
sem.sem_num = 0; //信号量的编号,第一个为 0
sem.sem_op = 1; //+1 表示执行 V 操作,生产产品
sem.sem_flg = 0; //或写 SEM_UNDO
union semun arg;
arg.val = 0; //初始化数据
semctl(semid, 0, SETALL, arg);
//将数据全部设置到信号量集里面去,相当于公共数据
while(1){
semop(semid, &sem, 1); //执行指定的 V 操作,表示生产产品
printf("productor total number:%d\n", semctl(semid, 0, GETVAL));//获得公共值
sleep(1);
}
}else{
sleep(2); //先让子进程有时间生产
struct sembuf sem; //定义信号量结构体
memset(&sem, 0, sizeof(struct sembuf));
sem.sem_num = 0; //信号量的编号,第一个为 0
sem.sem_op = -1; //-1 表示执行 P 操作,消费产品
sem.sem_flg = 0; //或写 SEM_UNDO
while(1){
semop(semid, &sem, 1);//执行指定的 P 操作消费产品
printf("costomer total number:%d\n", semctl(semid, 0, GETVAL));//获得公共值
sleep(2);
}
}
}
示例:没有亲缘关系的生产者消费者问题
//生产者源码:
#include
#include
#include
#include
#include
#include
#include
void init(); //initlization semaphore
void del(); //delete semaphore
int sem_id;
int main(int argc, char *argv[]){
struct sembuf sops[2];
/*set operate way for semaphore*/
sops[0].sem_num = 0; //第一个信号的编号,表示生产了几个产品
sops[0].sem_op = 1; //进行 V 操作
sops[0].sem_flg = 0; //或写为 SEM_UNDO
sops[1].sem_num = 1; //第二个信号编号,表示还可以生产几个产品
sops[1].sem_op = -1; //进行 P 操作
sops[1].sem_flg = 0; //或写为 SEM_UNDO
init(); //初始化操作
printf("this is a producor\n");
while(1){
printf("\n\nbefore produce\n");
printf("productor number is %d\n", semctl(sem_id, 0, GETVAL));
printf("space number is %d\n", semctl(sem_id, 1, GETVAL));
semop(sem_id, (struct sembuf*)&sops[1], 1);
printf("now producing .....\n");
semop(sem_id, (struct sembuf*)&sops[0], 1);
printf("space number is %d\n", semctl(sem_id, 1, GETVAL));
printf("productor number is %d\n", semctl(sem_id, 0, GETVAL));
sleep(2);
}
del();
}
void init(){
int ret;
unsigned short sem_array[2];
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
}arg;
sem_id = semget((key_t)1234, 2, IPC_CREAT|0644);
/*get semaphore include two sem*/
/*set sem's vaule*/
sem_array[0] = 0;
sem_array[1] = 10;
arg.array = sem_array;
ret = semctl(sem_id, 0, SETALL, arg);
//将所有 semun.array 的值设定到信号集中,第二个参数为 0
if(ret == -1){
printf("SETALL failed (%d)\n", errno);
}
printf("productor init is %d\n", semctl(sem_id, 0, GETVAL));
printf("space init is %d\n\n", semctl(sem_id, 1, GETVAL));
}
void del(){
semctl(sem_id, IPC_RMID, 0); //删除信号集
}
//消费者源码:
#include
#include
#include
#include
#include
#include
#include
void init();
int sem_id;
int main(int argc, char *argv[]){
struct sembuf sops[2];
/*set operate way for sem*/
sops[0].sem_num = 0;
sops[0].sem_op = -1;
//P 操作,表示有多少个产品可以消费(相当于生产了多少个产品)
sops[0].sem_flg = 0;
sops[1].sem_num = 1;
sops[1].sem_op = 1;
//V 操作,表示还可以生成的产品数
sops[1].sem_flg = 0;
init();
printf("this is a customer\n");
while(1){
printf("\n\nbefore consume\n");
printf("customer number is %d\n", semctl(sem_id, 0, GETVAL));
printf("space number is %d\n", semctl(sem_id, 1, GETVAL));
semop(sem_id, &sops[0], 1);
printf("now consume .....\n");
semop(sem_id, &sops[1], 1);
printf("space number is %d\n", semctl(sem_id, 1, GETVAL));
printf("customer number is %d\n", semctl(sem_id, 0, GETVAL));
sleep(1);
}
}
void init(){
sem_id = semget((key_t)1234, 2, IPC_CREAT|0644);
/*get semaphore include two sem*/
}
消息队列与 FIFO 很相似,都是一个队列结构,都可以有多个进程往队列里面写信息,
多个进程从队列中读取信息。但 FIFO 需要读、写的两端事先都打开,才能够开始信息传递工作。而消息队列可以事先往队列中写信息,需要时再打开读取信息。但是,消息队列先打开读,仍然会阻塞,因为此时没有消息可读。
System V IPC 机制:消息队列
//函数原型:
#include
#include
#include
//函数 msgget 创建和访问一个消息队列。该函数成功则返回一个唯一的消息队列标识符
//(类似于进程 ID 一样),失败则返回-1.
int msgget(key_t key, int msgflg);
//参数 key 是唯一标识一个消息队列的关键字,如果为 IPC_PRIVATE(值为 0),用创
//>>建一个只有创建者进程才可以访问的消息队列,可以用于父子间通信;
//>>非0值,可以通过ftok获得,表示可以被多个进程共享。
//参数 msgflg 指明队列的访问权限和创建标志,创建标志的可选值为 IPC_CREAT和 IPC_EXCL
//>>如果 IPC_EXCL 和 IPC_CREAT 同时指明,则要么创建新的消息队列,要么当队列存在时
//>>调用失败并返回-1。
//函数 msgsnd 和 msgrcv 用来将消息添加到消息队列中和从一个消息队列中获取信息。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
//参数 msgid 指明消息队列的 ID; 通常是 msgget 函数成功的返回值。
//参数 msgbuf 是消息结构体,它的长度必须小于系统规定的上限,必须以一个长整
//>>型成员变量开始,接收函数将用这个成员变量来确定消息的类型。必须重写这个结构体,
//>>其中第一个参数不能改,其他自定义。如下:
/*
struct msgbuf {
long mtype; // type of message
char mtext[1]; // message text, any type of data
};
*/
//参数 msgsz 是消息体的大小,每个消息体最大不要超过 4K;
//参数 msgflg 可以为 0(通常为 0)或 IPC_NOWAIT,如果设置 IPC_NOWAIT,则msgsnd 和 msgrcv 都不会阻塞,
//>>此时如果队列满并调用 msgsnd 或队列空时调用 msgrcv将返回错误;
//参数 msgtyp 有 3 种选项:
//msgtyp = 0 接收队列中的第 1 个消息(通常为 0)
//msgtyp > 0 接收对列中的第 1 个类型等于 msgtyp 的消息
//msgtyp < 0 接收其类型小于或等于 msgtyp 绝对值的第 1 个最低类型消息
//函数 msgctl 是消息队列的控制函数,常用来删除消息队列。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//参数 msqid 是由 msgget 返回的消息队列标识符。
//参数 cmd 通常为 IPC_RMID 表示删除消息队列。
//参数 buf 通常为 NULL 即可。
示例:有亲缘关系的消息队列(IPC_PRIVATE)
```cpp
#include
#include
#include
#include
#include
#include
#include
#include
struct MSG{
long mtype;
char buf[64];
};
int main(){
int msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
if(msgid == -1){
perror("msgget error");
exit(-1);
}
struct MSG msg;
memset(&msg,0,sizeof(struct MSG));
if(fork() > 0){
msg.mtype = 1;
strcpy(msg.buf,"hello");
msgsnd(msgid,&msg,sizeof(msg.buf),0);
wait(NULL);
msgctl(msgid, IPC_RMID, NULL);
exit(0);
}else{
sleep(2); //让父进程有时间往消息队列里面写
msgrcv(msgid, &msg, sizeof(msg.buf), 1, 0);
puts(msg.buf);
exit(0);
}
}
示例:没有亲缘关系
//消息发送
#include
#include
#include
#include
#include
#include
#include
#include
#define BUFFER 255
struct msgtype { //重新定义该结构体
long mtype; //第一个参数不变
char buffer[BUFFER + 1];
};
int main(int argc, char **argv){
int msgid = msgget((key_t)1234, 0666 | IPC_CREAT); //获取消息队列
if(msgid == -1){
fprintf(stderr, "Creat Message Error:%s\a\n", strerror(errno));
exit(1);
}
struct msgtype msg;
memset(&msg,0,sizeof(struct msgtype));
msg.mtype = 1; //给结构体的成员赋值
strncpy(msg.buffer,"hello",BUFFER);
msgsnd(msgid,&msg,sizeof(msg.buffer),0); //发送信号
memset(&msg,0, sizeof(msg)); //清空结构体
msgrcv(msgid,&msg, sizeof(msg.buffer), 2, 0); //接收信号
fprintf(stdout,"Client receive:%s\n",msg.buffer);
exit(0);
}
//消息接收
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BUFFER 255
struct msgtype { //重定义消息结构体
long mtype;
char buffer[BUFFER+1];
};
int main(){
int msgid = msgget((key_t)1234, 0666 | IPC_CREAT); //获得消息队列
if(msgid == -1){
fprintf(stderr,"Creat Message Error:%s\a\n",strerror(errno));
exit(1);
}
struct msgtype msg;
memset(&msg,0,sizeof(struct msgtype));
while(1){
msgrcv(msgid,&msg, sizeof(msg.buffer),1,0); //接收消息
fprintf(stdout,"Server Receive:%s\n",msg.buffer);
msg.mtype = 2;
strncpy(msg.buffer,"world",BUFFER);
msgsnd(msgid,&msg, sizeof(msg.buffer),0); //发送消息
}
exit(0);
}
System V 共享内存机制: shmget、shmat、shmdt、shmctl
原理及实现:system V IPC 机制下的共享内存本质是一段特殊的内存区域,进程间需要
共享的数据被放在该共享内存区域中,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。这样一个使用共享内存的进程可以将信息写入该空间,而另一个使用共享内存的进程又可以通过简单的内存读操作获取刚才写入的信息,使得两个不同进程之间进行了一次信息交换,从而实现进程间的通信。
//函数定义如下:
#include
#include
#include
//函数 ftok 用于创建一个关键字key(key_t),可以用该关键字关联一个共享内存段。
key_t ftok(const char *pathname, int proj_id);
//参数 pathname 为一个全路径文件名,并且该文件必须可访问。
//参数 proj_id 通常传入一非 0 字符
//通过 pathname 和 proj_id 组合可以创建唯一的 key
//如果调用成功,返回一关键字,否则返回-1
//函数 shmget 用于创建或打开一共享内存段,该内存段由函数的第一个参数唯一创建。
int shmget(key_t key, int size, int shmflg);
//函数成功,则返回一个唯一的共享内存标识号(相当于进程号,唯一的标识着共享内存),失败返回-1。
//参数 key 是一个与共享内存段相关联关键字,如果事先已经存在一个与指定关键字
//关联的共享内存段,则直接返回该内存段的标识,表示打开,如果不存在,则创建一个新的共享内存段。
//参数 size 指定共享内存段的大小,以字节为单位;
//参数 shmflg 是一掩码合成值,可以是访问权限值与(IPC_CREAT 或 IPC_EXCL)的
//>>合成。IPC_CREAT 表示如果不存在该内存段,则创建它。IPC_EXCL 表示如果该内存
//>>段存在,则函数返回失败结果(-1)。如果调用成功,返回内存段标识,否则返回-1
//函数 shmat 将共享内存段映射到进程空间的某一地址。
void *shmat(int shmid, const void *shmaddr, int shmflg);
//参数 shmid 是共享内存段的标识 通常应该是 shmget 的成功返回值
//参数 shmaddr 指定的是共享内存连接到当前进程中的地址位置。通常是 NULL,表
//>>示让系统来选择共享内存出现的地址。
//参数 shmflg 是一组位标识,通常为 0 即可。
//如果调用成功,返回映射后的进程空间的首地址,否则返回(char *)-1。
//函数 shmdt 用于将共享内存段与进程空间分离。
int shmdt(const void *shmaddr);
//参数 shmaddr 通常为 shmat 的成功返回值。
//函数成功返回 0,失败时返回-1.注意,将共享内存分离并没删除它,只是使得该共
//>>享内存对当前进程不在可用。
//函数 shmctl 是共享内存的控制函数,可以用来删除共享内存段。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//参数 shmid 是共享内存段标识 通常应该是 shmget 的成功返回值
//参数 cmd 是对共享内存段的操作方式,可选为 IPC_STAT,IPC_SET,IPC_RMID。通
//>>常为 IPC_RMID,表示删除共享内存段。
//参数 buf 是表示共享内存段的信息结构体数据,通常为 NULL。
//有进程连接,执行返回 0,标记删除成功,但是最后一个进程解除连接后,共享内存真正被删除。
Example:非亲进程间通信的实现步骤如下:
//写内存端
#include
#include
#include
#include
#include
#include //头文件包含
#include
main(){
key_t key = ftok("b.dat",1); //1. 写入端先用 ftok 函数获得 key
if(key == -1){
perror("ftok");
exit(-1);
}
int shmid = shmget(key,4096,IPC_CREAT); //2. 写入端用 shmget 函数创建一共享内存段
if(shmid == -1){
perror("shmget");
exit(-1);
}
char *pMap = (char *)shmat(shmid, NULL, 0); //3. 获得共享内存段的首地址
memset(pMap, 0, 4096);
strcpy(pMap, "hello world"); //4. 往共享内存段中写入内容
if(shmdt(pMap) == -1){ //5. 关闭共享内存段
perror("shmdt");
exit(-1);
}
}
//读内存端:
#include
#include
#include
#include
#include
#include
#include
main(){
key_t key = ftok("b.dat",1); //1. 读入端用 ftok 函数获得 key
if(key == -1){
perror("ftok");
exit(-1);
}
int shmid = shmget(key,4096,0600|IPC_CREAT); //2. 读入端用 shmget 函数打开共享内存段
if(shmid == -1){
perror("shmget");
exit(-1);
}
char *pMap = (char *)shmat(shmid, NULL, 0); //3. 获得共享内存段的首地址
printf("receive the data:%s\n",pMap); //4. 读取共享内存段中的内容
if(shmctl(shmid, IPC_RMID, 0) == -1){ //5. 删除共享内存段
perror("shmctl");
exit(-1);
}
}
示例,通过共享内存实现两个程序间的对话。
//程序shmwr.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
struct text{
int useful;
char buf[1024];
};
int main(){
int shmid = shmget((key_t)5080 , sizeof(struct text),0600|IPC_CREAT);
printf("%d \n" ,shmid);
struct text* ptext = (struct text *)shmat(shmid, NULL , 0);
// ptext->useful = 0 ;
while(1){
if(ptext -> useful == 0){
int iret = read(STDIN_FILENO, ptext->buf, 1024);
ptext->useful = 1;
if(strncmp("end", ptext->buf, 3)==0){
break;
}
//ptext ->useful = 0 ;
}
sleep(1);
}
shmdt((void *)ptext);
return 0 ;
}
//程序shmrd.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
struct text{
int useful;
char buf[1024];
};
int main(){
int shmid = shmget((key_t)5080, sizeof(struct text), 0600|IPC_CREAT);
struct text* ptext = (struct text *)shmat(shmid, NULL, 0);
ptext->useful = 0 ;
while(1){
if(ptext -> useful == 1){
write(STDOUT_FILENO, ptext -> buf, strlen(ptext -> buf));
ptext ->useful = 0 ;
if(strncmp("end", ptext->buf, 3)==0){
break;
}
}
sleep(1);
}
shmdt((void *)ptext);
shmctl(shmid, IPC_RMID, 0);
return 0 ;
}
TCP 通信的基本步骤如下:
服务端:socket—bind—listen—while(1){—accept—recv—send—close—}---close
客户端:socket----------------------------------connect—send—recv-----------------close
服务器端:
1. 头文件包含:
#include
#include
#include
#include
#include
#include
#include
#include
2. socket 函数:生成一个套接口描述符。
原型:int socket(int domain,int type,int protocol);
参数:domain→{ AF_INET:Ipv4 网络协议 AF_INET6:IPv6 网络协议}
type→{tcp:SOCK_STREAM udp:SOCK_DGRAM}
protocol→指定 socket 所使用的传输协议编号。通常为 0.
返回值:成功则返回套接口描述符,失败返回-1。
常用实例:int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){perror("socket");exit(-1);}
3. bind 函数:用来绑定一个端口号和 IP 地址,使套接口与指定的端口号和 IP 地址相关联。
原型:int bind(int sockfd, struct sockaddr * my_addr, socklen_t addrlen);
参数:sockfd→为前面 socket 的返回值。
my_addr→为结构体指针变量
addrlen→sockaddr 的结构体长度。通常是计算 sizeof(struct sockaddr);
struct sockaddr_in{ //常用的结构体
unsigned short int sin_family; //即为 sa_family ➔AF_INET
uint16_t sin_port; //为使用的 port 编号
struct in_addr sin_addr; //为 IP 地址
unsigned char sin_zero[8]; //未使用
};
struct in_addr{
uint32_t s_addr;
};
返回值:成功则返回 0,失败返回-1
常用实例:
struct sockaddr_in my_addr; //定义结构体变量
memset(&my_addr, 0, sizeof(struct sockaddr)); //将结构体清空
//bzero(&my_addr, sizeof(struct sockaddr));
my_addr.sin_family = AF_INET; //表示采用 Ipv4 网络协议
my_addr.sin_port = htons(8888); //表示端口号为 8888,通常是大于 1024 的一个值。
//htons()用来将参数指定的 16 位 hostshort 转换成网络字符顺序
my_addr.sin_addr.s_addr = inet_addr("192.168.0.101");
//inet_addr()用来将IP地址字符串转换成网络所使用的二进制数字
//如果为INADDR_ANY,这表示服务器自动填充本机IP地址。
if(bind(sfd, (struct sockaddr*)&my_str, sizeof(struct socketaddr)) == -1)
{perror("bind");close(sfd);exit(-1);}
(注:通过将 my_addr.sin_port 置为 0,函数会自动为你选择一个未占用的端口来使用。
同样,通过将my_addr.sin_addr.s_addr 置为 INADDR_ANY,系统会自动填入本机 IP 地址。)
4. listen 函数:使服务器的这个端口和 IP 处于监听状态,等待网络中某一客户机的连接请求。如果客户端有连接请求,端口就会接受这个连接。
原型:int listen(int sockfd,int backlog);
参数:sockfd→为前面 socket 的返回值.即 sfd
backlog→指定同时能处理的最大连接要求,通常为 10 或者 5。最大值可设至 128
返回值:成功则返回 0,失败返回-1
常用实例:
if(listen(sfd, 10) == -1)
{perror("listen");close(sfd);exit(-1);}
5. accept 函数:接受远程计算机的连接请求,建立起与客户机之间的通信连接。服务器处于监听状态时,如果某时刻获得客户机的连接请求,
此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求。当 accept 函数接受一个连接时,
会返回一个新的 socket 标识符,以后的数据传输和读取就要通过这个新的 socket 编号来处理,原来参数中的 socket 也可以继续使用,
继续监听其它客户机的连接请求。
原型:int accept(int s,struct sockaddr * addr,socklen_t* addrlen);
参数:
s→为前面 socket 的返回值.即 sfd
addr→为结构体指针变量,和 bind 的结构体是同种类型的,系统会把远程主机的信息(远程主机的地址和端口号信息)保存到这个指针所指的结构体中。
addrlen→表示结构体的长度,为整型指针
返回值:成功则返回新的 socket 处理代码 new_fd,失败返回-1
常用实例:
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);
if(new_fd == -1){perror("accept");close(sfd);exit(-1);}
printf("%s %d success connect\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
6. recv 函数:用新的套接字来接收远端主机传来的数据,并把数据存到由参数 buf 指向的内存空间
原型:int recv(int sockfd,void *buf,int len,unsigned int flags);
参数:
sockfd→为前面 accept 的返回值.即 new_fd,也就是新的套接字。
buf→表示缓冲区
len→表示缓冲区的长度
flags→通常为 0
返回值:成功则返回实际接收到的字符数,可能会少于你所指定的接收长度。失败返回-1
常用实例:
char buf[512] = {0};
if(recv(new_fd, buf, sizeof(buf), 0) == -1)
{perror("recv");close(new_fd);close(sfd);exit(-1);}
puts(buf);
7. send 函数:用新的套接字发送数据给指定的远端主机
原型:int send(int s,const void * msg,int len,unsigned int flags);
参数:
s→为前面 accept 的返回值.即 new_fd
msg→一般为常量字符串
len→表示长度
flags→通常为 0
返回值:成功则返回实际传送出去的字符数,可能会少于你所指定的发送长度。失败返回-1
常用实例:if(send(new_fd, "hello", 6, 0) == -1)
{perror("send");close(new_fd);close(sfd);exit(-1);}
8. close 函数:当使用完文件后若已不再需要则可使用 close()关闭该文件,并且 close()会让数据写回磁盘,并释放该文件所占用的资源
原型:int close(int fd);
参数:fd→为前面的 sfd,new_fd
返回值:若文件顺利关闭则返回 0,发生错误时返回-1
常用实例:
close(new_fd);
close(sfd);
//可以调用 shutdown 实现半关闭
int shutdown(int sockfd, int how);
客户端:
1. connect 函数:用来请求连接远程服务器,将参数 sockfd 的 socket 连至参数 serv_addr 指定的服务器IP 和端口号上去。
原型:int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
参数:
sockfd→为前面 socket 的返回值,即 sfd
serv_addr→为结构体指针变量,存储着远程服务器的 IP 与端口号信息。
addrlen→表示结构体变量的长度
返回值:成功则返回 0,失败返回-1
常用实例:
struct sockaddr_in seraddr;//请求连接服务器
memset(&seraddr, 0, sizeof(struct sockaddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8888); //服务器的端口号
seraddr.sin_addr.s_addr = inet_addr("192.168.0.101"); //服务器的 ip
if(connect(sfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr)) == -1)
{perror("connect");close(sfd);exit(-1);}
Example:将一些通用的代码全部封装起来,以后要用直接调用函数即可。如下:
//tcp_net_socket.h
#ifndef __TCP__NET__SOCKET__H
#define __TCP__NET__SOCKET__H
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern int tcp_init(const char* ip,int port);
extern int tcp_accept(int sfd);
extern int tcp_connect(const char* ip,int port);
extern void signalhandler(void);
#endif
//具体的通用函数封装如下:tcp_net_socket.c
#include "tcp_net_socket.h"
int tcp_init(const char* ip, int port){ //用于初始化操作
int sfd = socket(AF_INET, SOCK_STREAM, 0);//首先创建一个 socket,向系统申请
if(sfd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = inet_addr(ip);//或 INADDR_ANY
if(bind(sfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1){
//将新的 socket 与制定的 ip、port 绑定
perror("bind");
close(sfd);
exit(-1);
}
if(listen(sfd, 10) == -1){//监听它,并设置其允许最大的连接数为 10 个
perror("listen");
close(sfd);
exit(-1);
}
return sfd;
}
int tcp_accept(int sfd) {//用于服务端的接收
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);
//sfd 接受客户端连接,并创建新的 socket 为 new_fd,将请求连接的客户端的 ip、port 保存在结构体clientaddr 中
if(new_fd == -1){
perror("accept");
close(sfd);
exit(-1);
}
printf("%s %d connect success ...\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
return new_fd;
}
int tcp_connect(const char* ip, int port) {//用于客户端的连接
int sfd = socket(AF_INET, SOCK_STREAM, 0);//向系统注册申请新的 socket
if(sfd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = inet_addr(ip);
if(connect(sfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1){
//将 sfd 连接至制定的服务器网络地址 serveraddr
perror("connect");
close(sfd);
exit(-1);
}
return sfd;
}
void signalhandler(void) {//用于信号处理,让服务端在按下 Ctrl+c 或 Ctrl+\的时候不会退出
sigset_t sigSet;
sigemptyset(&sigSet);
sigaddset(&sigSet,SIGINT);
sigaddset(&sigSet,SIGQUIT);
sigprocmask(SIG_BLOCK,&sigSet,NULL);
}
Linux下KILL函数的用法 — 参考链接
Kill函数和Kill命令 — 参考链接
kill命令: 用于向任何进程组或进程发送信号。
头文件用法:
#include
#include
函数原型:
int kill(pid_t pid, int sig);
参数:
pid:可能选择有以下四种
参数:sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在执行。
kill、killall
这两个命令是用来向进程发送信号的。kill 命令需要进程号作为参数,而 killall 需要进程名称。
另外,还可以在这两个命令后附加要发送信号序号作为参数。默认情况下,它们都向相关进程发送信号 15 (TERM)。
例如,如果你想要终止 PID 为 785 的进程,请输入以下命令:
$ kill 785
如果您要向它发送信号 19 (STOP),请输入:
$ kill -19 785
假设您知道想要终止的进程的命令名称。您可以通过该名称来终止它,而不用再使用 ps 找出该进程的进程号:
$ killall -9 mozilla
需要说明的是,一个进程并不是向任何进程均能发送信号的,这里有一个限制,就是普通用户的进程 只能向具有与其相同的用户标识符的进程发送信号,也就是说,一个用户的进程不能向另一个用户的进程发送信号。只有root用户的进程能够给任何进程发送信号。
进程组:进程组是一个或多个进程的集合,通常它们与一组作业相关联,可以接受来自同一终端的各种信号。每个进程组都有一个组长,进程组的ID和进程组长ID一致。
权限保护:root用户可以发送信号给任何用户,而普通信号不可以向系统用户(的进程)或者其他普通用户(的进程)发送任何信号。普通用户只可以向自己创建的进程发送信号。
它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数
信号的生命周期?
信号产生 -> 信号在进程中注册 -> 信号在进程中的注销 -> 执行信号处理函数
信号的产生方式?
(1)当用户按某些终端键时产生信号
(2)硬件异常产生信号【内存非法访问】
(3)软件异常产生信号【某一个条件达到时】
(4)调用kill函数产生信号【接受和发送的所有者必须相同,或者发送的进程所有者必须为超级用户】
(5)运行kill命令产生信号
信号处理方式?
(1)执行默认处理方式
(2)忽略处理
(3)执行用户自定义的函数
什么是协程 — 参考博客
用户态的轻量级线程,有自己的寄存器和栈
协程,英文名是 Coroutine, 又称为微线程,是一种用户态的轻量级线程。协程不像线程和进程那样,需要进行系统内核上的上下文切换,协程的上下文切换是由程序员决定的。
多线程编程是比较困难的, 因为调度程序任何时候都能中断线程, 必须记住保留锁, 去保护程序中重要部分, 防止多线程在执行的过程中断。
而协程默认会做好全方位保护, 以防止中断。我们必须显示产出才能让程序的余下部分运行。对协程来说, 无需保留锁, 而在多个线程之间同步操作, 协程自身就会同步, 因为在任意时刻, 只有一个协程运行。总结下大概下面几点:
无法使用 CPU 的多核
协程的本质是个单线程,它不能同时用上单个 CPU 的多个核,协程需要和进程配合才能运行在多 CPU上。当然我们日常所编写的绝大部分应用都没有这个必要,就比如网络爬虫来说,限制爬虫的速度还有其他的因素,比如网站并发量、网速等问题都会是爬虫速度限制的因素。除非做一些密集型应用,这个时候才可能会用到多进程和协程。
处处都要使用非阻塞代码
写协程就意味着你要一直写一些非阻塞的代码,使用各种异步版本的库,比如后面的异步爬虫教程中用的 aiohttp 就是一个异步版本的request库等。 不过这些缺点并不能影响到使用协程的优势。
虚拟内存的定义及实现方式 — 参考链接
一次性
作业必须一次性全部装入内存后,方能开始运行。这会导致两种情况发生:
驻留性
作业被装入内存后,就一直驻留在内存中,其任何部分都不会被换出,直至作业运行结束。运行中的进程,会因等待I/O而被阻塞,可能处于长期等待状态。
由以上分析可知,许多在程序运行中不用或暂时不用的程序(数据)占据了大量的内存空间,而一些需要运行的作业又无法装入运行,显然浪费了宝贵的内存资源。
要真正理解虚拟内存技术的思想,首先必须了解计算机中著名的局部性原理。著名的 Bill Joy (SUN公司CEO)说过:”在研究所的时候,我经常开玩笑地说高速缓存是计算机科学中唯一重要的思想。事实上,髙速缓存技术确实极大地影响了计算机系统的设计。“快表、页高速缓存以及虚拟内存技术从广义上讲,都是属于高速缓存技术。这个技术所依赖的原理就是局部性原理。局部性原理既适用于程序结构,也适用于数据结构(更远地讲,Dijkstra 著名的关于“goto语句有害”的论文也是出于对程序局部性原理的深刻认识和理解)。
局部性原理表现在以下两个方面:
时间局部性是通过将近来使用的指令和数据保存到高速缓存存储器中,并使用高速缓存的层次结构实现。空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。虚拟内存技术实际上就是建立了 “内存一外存”的两级存储器的结构,利用局部性原理实现髙速缓存。
基于局部性原理,在程序装入时,可以将程序的一部分装入内存,而将其余部分留在外存,就可以启动程序执行。在程序执行过程中,当所访问的信息不在内存时,由操作系统将所需要的部分调入内存,然后继续执行程序。另一方面,操作系统将内存中暂时不使用的内容换出到外存上,从而腾出空间存放将要调入内存的信息。这样,系统好像为用户提供了一个比实际内存大得多的存储器,称为虚拟存储器。
之所以将其称为虚拟存储器,是因为这种存储器实际上并不存在,只是由于(对用户完全透明),给用户的感觉是好像存在一个比实际物理内存大得多的存储器。虚拟存储器的大小由计算机的地址结构决定,并非是内存和外存的简单相加。虚拟存储器有以下三个主要特征:
虚拟内存中,允许将一个作业分多次调入内存。釆用连续分配方式时,会使相当一部分内存空间都处于暂时或“永久”的空闲状态,造成内存资源的严重浪费,而且也无法从逻辑上扩大内存容量。因此,虚拟内存的实现需要建立在离散分配的内存管理方式的基础上。虚拟内存的实现有以下三种方式:
确保线程安全的几种方法 — 参考链接
竞争与原子操作
____多个线程同时访问和修改一个数据,可能造成很严重的后果。出现严重后果的原因是很多操作被操作系统编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断了而去执行别的代码了。一般将单指令的操作称为原子的(Atomic),因为不管怎样,单条指令的执行是不会被打断的。
____因此,为了避免出现多线程操作数据的出现异常,Linux系统提供了一些常用操作的原子指令,确保了线程的安全。但是,它们只适用于比较简单的场合,在复杂的情况下就要选用其他的方法了。
同步与锁
____为了避免多个线程同时读写一个数据而产生不可预料的后果,开发人员要将各个线程对同一个数据的访问同步,也就是说,在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。
____同步的最常用的方法是使用锁(Lock),它是一种非强制机制,每个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁;在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。
____二元信号量是最简单的一种锁,它只有两种状态:占用与非占用,它适合只能被唯一·一个线程独占访问的资源。对于允许多个线程并发访问的资源,要使用多元信号量(简称信号量)。
可重入
____一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进入该函数执行。一个函数称为可重入的,表明该函数被重入之后不会产生任何不良后果。可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。
阻止过度优化—volatile
____在很多情况下,即使我们合理地使用了锁,也不一定能够保证线程安全,因此,我们可能对代码进行过度的优化以确保线程安全。
____我们可以使用volatile关键字试图阻止过度优化,它可以做两件事:第一,阻止编译器为了提高速度将一个变量缓存到寄存器而不写回;第二,阻止编译器调整操作volatile变量的指令顺序。
____在另一种情况下,CPU的乱序执行让多线程安全保障的努力变得很困难,通常的解决办法是调用CPU提供的一条常被称作barrier的指令,它会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。
TCP/IP五层模型 – 参考链接
协议分层 — 参考链接
因特网是一个非常复杂的系统,有大量的应用程序、协议以及各种端系统、链路、分组交换机等。这种复杂性,给我们开发使用互联网的协议提供了一定的困难。针对这个问题,大佬们通过协议分层的概念把因特网这个复杂的系统分成了若干个层次,使其模块化,从而方便大家对因特网的理解。
所谓的协议分层,就是根据互联网所需要的服务和功能,在体系结构上分成若干个层次,协议的服务和功能与哪一层的服务和功能相对应,该协议就属于哪一层。每层协议层通过在该层中执行某些动作或使用直接下层的服务来提供服务。协议分层具有概念化和结构化的特点,通过协议分层来研究讨论系统组件,便于封装,会使系统组件的更新更容易。
每一层都呼叫它的下一层提供的网络来完成自己的需求。
注意:如果是四层模型数据链路层和物理层在一层
传输层和网络层的封装在操作系统完成。应用层的封装在应用程序中完成。
数据链路层和物理层的封装在设备驱动程序与网络接口中完成。
关系:
一般而言:
DHCP协议浅析 — 参考链接
DHCP协议:动态主机配置协议
客户端端口:68;服务端端口:67
交互过程
DHCP 服务器
DHCP服务器收到来自客户端的DHCP REQUEST报文,而只有符合服务器标识这个选项字段的DHCP服务器才会对此作出响应:
如果服务器可以分配此IP则以DHCP ACK报文进行响应。
如果服务器无法分配此IP则以DHCP NAK报文进行响应。
其他DHCP服务器则清除与此请求相关的状态。响应方式还是跟提供阶段一样广播此报文。
DHCP客户端
当DHCP客户端收到的响应是DHCP NAK报文,则会重新发送 DHCP DISCOVER报文。
若收到的是DHCP ACK报文则会执行地址冲突检测(ACD)探测获得的IP地址是否未被使用。
如果已经被使用则向DHCP服务器发送一个DHCP DECLINE报文以通知该地址已经不能被使用。之后经过默认的10秒延时后客户端可再次重试。
如果未被使用则获得了该IP地址在租用期间的使用权。
大端,历史遗留问题
ICMP协议
信道利用率太低,每次都需要等上一次ACK包接收到了才能再次发送
(1)慢开始(2)拥塞避免(3)快重传【收到3个失序分组确认】(4)快恢复
(1)客户机的应用程序调用解析程序将域名以UDP数据报的形式发给本地DNS服务器
(2)本地DNS服务器找到对应IP以UDP形式发送回来
(3)若本地DNS服务器找不到,则需要将域名发送到根域名服务器,根域名服务器返回下一个要访问的域名服务器,则访问下一个域名服务器。
(1)申请空的PCB
(2)为新进程分配资源
(3)初始化PCB
(4)将新进程插入就绪队列中
原因:
步骤:
权限不够
可以接受。
数据:零窗口探测报文;确认报文段;携带紧急数据的报文段
可能会被抛弃
开启计时器,发送零窗口探测报文
全局和局部;
全局:在整个内存空间置换
局部:在本进程中进行置换
全局:(1)工作集算法(2)缺页率置换算法
局部:(1)最优算法(无法实现)(2)FIFO先进先出算法(3)LRU最近最久未使用(4)时钟轮转算法
(1)一个在用户地址空间执行;一个在内核空间执行
(2)一个是过程调用,开销小;一个需要切换用户空间和内核上下文,开销大
(3)一般相同;不同系统不同
一个会丢失,另外一个则会用队列来保存相应的事件
原因:系统资源不足;进程运行推进顺序不合适;资源分配不当
条件:互斥;不剥夺;循环等待;请求与保持
预防:破坏任意一个条件
避免:银行家算法是一种经典的死锁避免算法,银行家相当于操作系统,资金是资源,进程是客户,银行家算法是一种最有代表性的避免死锁的算法。在避免死锁方法中允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。
检测:资源分配图简化法
(1)进程又自己的独立地址空间,线程没有
(2)进程是资源分配的最小单位,线程是CPU调度的最小单位
(3)进程和线程通信方式不同
(4)进程切换上下文开销大,线程开销小
(5)一个进程挂掉了不会影响其他进程,而线程挂掉了会影响其他线程
(6)对进程进程操作一般开销都比较大,对线程开销就小了
栈上:分配简单,只需要移动栈顶指针,不需要其他的处理
堆上:分配复杂,需要进行一定程度清理工作,同时是调用函数处理的。
TCP的三次握手最主要是防止已过期的连接再次传到被连接的主机。
如果采用两次的话,会出现下面这种情况。
比如是A机要连到B机,结果发送的连接信息由于某种原因没有到达B机;于是,A机又发了一次,结果这次B收到了,于是就发信息回来,两机就连接。传完东西后,断开。
结果这时候,原先没有到达的连接信息突然又传到了B机,于是B机发信息给A,然后B机就以为和A连上了,这个时候B机就在等待A传东西过去。
三次握手改成仅需要两次握手,死锁是可能发生。
考虑计算机A和B之间的通信,假定B给A发送一个连接请求分组,A收到了这个分组,并发送了确认应答分组。按照两次握手的协定,A认为连接已经成功地建立了,可以开始发送数据分组。可是,A的应答分组在传输中丢失,B不知道A是否已准备好,不知道A建议什么样的序列号,B甚至怀疑A是否收到自己的连接请求分组。在这种情况下,B认为连接还未建立成功,将忽略A发来的任何数据分组,只等待连接确认应答分组。而A在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。
————————————————————————————————————————————————————————————
分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线分割线
————————————————————————————————————————————————————————————
linux C/C++服务器后台开发面试题总结
半同步半异步编程
半同步半异步I/O的设计模式(half sync/half async)
Linux服务器–两种高效的并发模式(半同步/半异步模式、领导者/追随者模式)
数据库相关
Linux C++后台开发面试题目汇总
深信服面试题
深信服 linux软件开发面试题整理
Linux的多线程的一些面试题
深入了解TCP与UDP
Linux的网络编程面试题汇总
知乎的总结
C++ 后台开发面试时一般考察什么?